Skip to content

Commit

Permalink
Merge pull request #42252 from snazy/qml-no-exit-code
Browse files Browse the repository at this point in the history
Fix `QuarkusMainLauncher` not returning exit code
  • Loading branch information
gsmet authored Aug 1, 2024
2 parents 92eef40 + 8e7c255 commit 85417b0
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<Integer>() {
@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<Integer>) result::set);
setAlreadyStartedCallback.invoke(null, (Consumer<Boolean>) 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);
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Integer, Throwable> defaultExitCodeHandler = new BiConsumer<Integer, Throwable>() {
private static final BiConsumer<Integer, Throwable> MAIN_EXIT_CODE_HANDLER = new BiConsumer<>() {
@Override
public void accept(Integer integer, Throwable cause) {
Logger logger = Logger.getLogger(Application.class);
Expand All @@ -62,6 +61,12 @@ public void accept(Integer integer, Throwable cause) {
System.exit(integer);
}
};
private static final Consumer<Boolean> NOOP_ALREADY_STARTED_CALLBACK = new Consumer<>() {
@Override
public void accept(Boolean t) {
}
};
private static volatile BiConsumer<Integer, Throwable> defaultExitCodeHandler = MAIN_EXIT_CODE_HANDLER;

private ApplicationLifecycleManager() {

Expand All @@ -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<Boolean> 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");
Expand All @@ -89,17 +95,19 @@ public static void run(Application application, String... args) {

public static void run(Application application, Class<? extends QuarkusApplication> quarkusApplication,
BiConsumer<Integer, Throwable> 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;
Expand Down Expand Up @@ -209,6 +217,7 @@ public static void run(Application application, Class<? extends QuarkusApplicati
int exceptionExitCode = rootCause instanceof PreventFurtherStepsException
? ((PreventFurtherStepsException) rootCause).getExitCode()
: 1;
currentApplication = null;
(exitCodeHandler == null ? defaultExitCodeHandler : exitCodeHandler).accept(exceptionExitCode, e);
return;
} finally {
Expand Down Expand Up @@ -352,7 +361,9 @@ public static boolean isVmShuttingDown() {
* @param defaultExitCodeHandler the new default exit handler
*/
public static void setDefaultExitCodeHandler(BiConsumer<Integer, Throwable> defaultExitCodeHandler) {
Objects.requireNonNull(defaultExitCodeHandler);
if (defaultExitCodeHandler == null) {
defaultExitCodeHandler = MAIN_EXIT_CODE_HANDLER;
}
ApplicationLifecycleManager.defaultExitCodeHandler = defaultExitCodeHandler;
}

Expand All @@ -365,8 +376,18 @@ public static void setDefaultExitCodeHandler(BiConsumer<Integer, Throwable> defa
*
* @param defaultExitCodeHandler the new default exit handler
*/
// Used by StartupActionImpl via reflection
public static void setDefaultExitCodeHandler(Consumer<Integer> defaultExitCodeHandler) {
setDefaultExitCodeHandler((exitCode, cause) -> defaultExitCodeHandler.accept(exitCode));
BiConsumer<Integer, Throwable> biConsumer = defaultExitCodeHandler == null ? null
: (exitCode, cause) -> defaultExitCodeHandler.accept(exitCode);
setDefaultExitCodeHandler(biConsumer);
}

@SuppressWarnings("unused")
// Used by StartupActionImpl via reflection
public static void setAlreadyStartedCallback(Consumer<Boolean> alreadyStartedCallback) {
ApplicationLifecycleManager.alreadyStartedCallback = alreadyStartedCallback != null ? alreadyStartedCallback
: NOOP_ALREADY_STARTED_CALLBACK;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Integer> {

@CommandLine.Option(names = "--code")
int exitCode;

@Override
public Integer call() {
return exitCode;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" })
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Integer> {

@CommandLine.Option(names = "--code")
int exitCode;

@Override
public Integer call() {
return exitCode;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit 85417b0

Please sign in to comment.