Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to handle both exit code and exception when running CLI applications #12532

Merged
merged 1 commit into from
Oct 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -285,9 +285,6 @@ void build(List<StaticBytecodeRecorderBuildItem> staticInitTasks,
activeProfile,
tryBlock.load(LaunchMode.DEVELOPMENT.equals(launchMode.getLaunchMode())));
cb = tryBlock.addCatch(Throwable.class);
cb.invokeVirtualMethod(ofMethod(Logger.class, "errorv", void.class, Throwable.class, String.class, Object.class),
cb.readStaticField(logField.getFieldDescriptor()), cb.getCaughtException(),
cb.load("Failed to start application (with profile {0})"), activeProfile);

// an exception was thrown before logging was actually setup, we simply dump everything to the console
ResultHandle delayedHandler = cb
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;

import io.quarkus.bootstrap.BootstrapConstants;

Expand All @@ -23,7 +22,7 @@
*/
public class QuarkusLauncher {

public static void launch(String callingClass, String quarkusApplication, Consumer<Integer> exitHandler, String... args) {
public static void launch(String callingClass, String quarkusApplication, String... args) {
try {
String classResource = callingClass.replace(".", "/") + ".class";
URL resource = Thread.currentThread().getContextClassLoader().getResource(classResource);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.Set;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

import javax.enterprise.context.spi.CreationalContext;
Expand All @@ -17,6 +18,7 @@
import org.jboss.logging.Logger;
import org.wildfly.common.lock.Locks;

import io.quarkus.runtime.configuration.ProfileManager;
import io.quarkus.runtime.graal.DiagnosticPrinter;
import sun.misc.Signal;
import sun.misc.SignalHandler;
Expand All @@ -28,7 +30,7 @@
* but nothing else. This class can be used to run both persistent applications that will run
* till they receive a signal, and command mode applications that will run until the main method
* returns. This class registers a shutdown hook to properly shut down the application, and handles
* exiting with the supplied exit code.
* exiting with the supplied exit code as well as any exception thrown when starting the application.
*
* This class should be used to run production and dev mode applications, while test use cases will
* likely want to just use {@link Application} directly.
Expand All @@ -38,9 +40,9 @@
*/
public class ApplicationLifecycleManager {

private static volatile Consumer<Integer> defaultExitCodeHandler = new Consumer<Integer>() {
private static volatile BiConsumer<Integer, Throwable> defaultExitCodeHandler = new BiConsumer<Integer, Throwable>() {
@Override
public void accept(Integer integer) {
public void accept(Integer integer, Throwable cause) {
System.exit(integer);
}
};
Expand All @@ -67,7 +69,7 @@ public static void run(Application application, String... args) {
}

public static void run(Application application, Class<? extends QuarkusApplication> quarkusApplication,
Consumer<Integer> exitCodeHandler, String... args) {
BiConsumer<Integer, Throwable> exitCodeHandler, String... args) {
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
Expand Down Expand Up @@ -136,10 +138,9 @@ public static void run(Application application, Class<? extends QuarkusApplicati
}
}
} catch (Exception e) {
if (appStarted) {
//we only log if the error occurred after the application was started
//as the generated application class already has logging
Logger.getLogger(Application.class).error("Error running Quarkus application", e);
if (exitCodeHandler == null) {
Logger.getLogger(Application.class).errorv(e, "Failed to start application (with profile {0})",
ProfileManager.getActiveProfile());
}
stateLock.lock();
try {
Expand All @@ -149,28 +150,28 @@ public static void run(Application application, Class<? extends QuarkusApplicati
stateLock.unlock();
}
application.stop();
(exitCodeHandler == null ? defaultExitCodeHandler : exitCodeHandler).accept(1);
(exitCodeHandler == null ? defaultExitCodeHandler : exitCodeHandler).accept(1, e);
return;
}
if (!alreadyStarted) {
application.stop(); //this could have already been called
}
(exitCodeHandler == null ? defaultExitCodeHandler : exitCodeHandler).accept(getExitCode()); //this may not be called if shutdown was initiated by a signal
(exitCodeHandler == null ? defaultExitCodeHandler : exitCodeHandler).accept(getExitCode(), null); //this may not be called if shutdown was initiated by a signal
}

private static void registerHooks(final Consumer<Integer> exitCodeHandler) {
private static void registerHooks(final BiConsumer<Integer, Throwable> exitCodeHandler) {
if (ImageInfo.inImageRuntimeCode() && System.getenv(DISABLE_SIGNAL_HANDLERS) == null) {
registerSignalHandlers(exitCodeHandler);
}
final ShutdownHookThread shutdownHookThread = new ShutdownHookThread();
Runtime.getRuntime().addShutdownHook(shutdownHookThread);
}

private static void registerSignalHandlers(final Consumer<Integer> exitCodeHandler) {
private static void registerSignalHandlers(final BiConsumer<Integer, Throwable> exitCodeHandler) {
final SignalHandler exitHandler = new SignalHandler() {
@Override
public void handle(Signal signal) {
exitCodeHandler.accept(signal.getNumber() + 0x80);
exitCodeHandler.accept(signal.getNumber() + 0x80, null);
}
};
final SignalHandler diagnosticsHandler = new SignalHandler() {
Expand Down Expand Up @@ -209,7 +210,7 @@ public static void exit() {
exit(-1);
}

public static Consumer<Integer> getDefaultExitCodeHandler() {
public static BiConsumer<Integer, Throwable> getDefaultExitCodeHandler() {
return defaultExitCodeHandler;
}

Expand All @@ -222,19 +223,32 @@ public static boolean isVmShuttingDown() {
}

/**
* Sets the default exit code handler for application run through the run method
* Sets the default exit code and exception handler for application run through the run method
* that does not take an exit handler.
*
* By default this will just call System.exit, however this is not always
* what is wanted.
*
* @param defaultExitCodeHandler the new default exit handler
*/
public static void setDefaultExitCodeHandler(Consumer<Integer> defaultExitCodeHandler) {
public static void setDefaultExitCodeHandler(BiConsumer<Integer, Throwable> defaultExitCodeHandler) {
Objects.requireNonNull(defaultExitCodeHandler);
ApplicationLifecycleManager.defaultExitCodeHandler = defaultExitCodeHandler;
}

/**
* Sets the default exit code handler for application run through the run method
* that does not take an exit handler.
*
* By default this will just call System.exit, however this is not always
* what is wanted.
*
* @param defaultExitCodeHandler the new default exit handler
*/
public static void setDefaultExitCodeHandler(Consumer<Integer> defaultExitCodeHandler) {
setDefaultExitCodeHandler((exitCode, cause) -> defaultExitCodeHandler.accept(exitCode));
}

/**
* Signals that the application should exit with the given code.
*
Expand Down
22 changes: 10 additions & 12 deletions core/runtime/src/main/java/io/quarkus/runtime/Quarkus.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.quarkus.runtime;

import java.util.function.Consumer;
import java.util.function.BiConsumer;

import org.jboss.logging.Logger;
import org.jboss.logmanager.LogManager;
Expand Down Expand Up @@ -46,10 +46,11 @@ public static void run(Class<? extends QuarkusApplication> quarkusApplication, S
* go into the QuarkusApplication.
*
* @param quarkusApplication The application to run, or null
* @param exitHandler The handler that is called with the exit code when the application has finished
* @param exitHandler The handler that is called with the exit code and any exception (if any) thrown when the application
* has finished
* @param args The command line parameters
*/
public static void run(Class<? extends QuarkusApplication> quarkusApplication, Consumer<Integer> exitHandler,
public static void run(Class<? extends QuarkusApplication> quarkusApplication, BiConsumer<Integer, Throwable> exitHandler,
String... args) {
try {
System.setProperty("java.util.logging.manager", LogManager.class.getName());
Expand All @@ -63,25 +64,23 @@ public static void run(Class<? extends QuarkusApplication> quarkusApplication, C
} catch (ClassNotFoundException e) {
//ignore, this happens when running in dev mode
} catch (Exception e) {
//TODO: exception mappers
Logger.getLogger(Quarkus.class).error("Error running Quarkus", e);
if (exitHandler != null) {
exitHandler.accept(1);
exitHandler.accept(1, e);
} else {
ApplicationLifecycleManager.getDefaultExitCodeHandler().accept(1);
Logger.getLogger(Quarkus.class).error("Error running Quarkus", e);
ApplicationLifecycleManager.getDefaultExitCodeHandler().accept(1, e);
}
return;
}

//dev mode path, i.e. launching from the IDE
//this is not the quarkus:dev path as it will augment before
//calling this method
launchFromIDE(quarkusApplication, exitHandler, args);
launchFromIDE(quarkusApplication, args);

}

private static void launchFromIDE(Class<? extends QuarkusApplication> quarkusApplication, Consumer<Integer> exitHandler,
String... args) {
private static void launchFromIDE(Class<? extends QuarkusApplication> quarkusApplication, String... args) {
//some trickery, get the class that has invoked us, and use this to figure out the
//classes root
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
Expand All @@ -90,8 +89,7 @@ private static void launchFromIDE(Class<? extends QuarkusApplication> quarkusApp
pos++;
}
String callingClass = stackTrace[pos].getClassName();
QuarkusLauncher.launch(callingClass, quarkusApplication == null ? null : quarkusApplication.getName(), exitHandler,
args);
QuarkusLauncher.launch(callingClass, quarkusApplication == null ? null : quarkusApplication.getName(), args);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package io.quarkus.runtime.graal;

import java.util.function.Consumer;

import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;

Expand All @@ -12,8 +10,7 @@
final class QuarkusSubstitution {

@Substitute
private static void launchFromIDE(Class<? extends QuarkusApplication> quarkusApplication, Consumer<Integer> exitHandler,
String... args) {
private static void launchFromIDE(Class<? extends QuarkusApplication> quarkusApplication, String... args) {
throw new RuntimeException("Should never be hit");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.quarkus.commandmode;

import org.assertj.core.api.Assertions;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusProdModeTest;

public class ExceptionHandlingCommandModeTestCase {

@RegisterExtension
static final QuarkusProdModeTest config = new QuarkusProdModeTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addAsManifestResource("application.properties", "microprofile-config.properties")
.addClasses(ThrowExceptionApplicationMain.class, ThrowExceptionApplication.class))
.setApplicationName("exception-handling")
.setApplicationVersion("0.1-SNAPSHOT")
.setExpectExit(true)
.setRun(true);

@Test
public void testRun() {
Assertions.assertThat(config.getStartupConsoleOutput())
.contains("exception-handling").contains("Exception and exit code [1] handled by application");
Assertions.assertThat(config.getExitCode()).isEqualTo(10);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
import java.util.function.BiConsumer;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
Expand All @@ -25,12 +25,13 @@ public class ExitCodeTestCase {
@Test
public void testReturnedExitCode() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future = new CompletableFuture<>();
ApplicationLifecycleManager.run(Application.currentApplication(), ExitCodeApplication.class, new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
future.complete(integer);
}
}, "5");
ApplicationLifecycleManager.run(Application.currentApplication(), ExitCodeApplication.class,
new BiConsumer<Integer, Throwable>() {
@Override
public void accept(Integer integer, Throwable cause) {
future.complete(integer);
}
}, "5");
Assertions.assertEquals(5, future.get());
}

Expand All @@ -41,9 +42,9 @@ public void testWaitToExitWithCode() throws ExecutionException, InterruptedExcep
@Override
public void run() {
ApplicationLifecycleManager.run(Application.currentApplication(), WaitToExitApplication.class,
new Consumer<Integer>() {
new BiConsumer<Integer, Throwable>() {
@Override
public void accept(Integer integer) {
public void accept(Integer integer, Throwable cause) {
future.complete(integer);
}
});
Expand All @@ -62,9 +63,9 @@ public void testWaitToExitWithNoCode() throws ExecutionException, InterruptedExc
@Override
public void run() {
ApplicationLifecycleManager.run(Application.currentApplication(), WaitToExitApplication.class,
new Consumer<Integer>() {
new BiConsumer<Integer, Throwable>() {
@Override
public void accept(Integer integer) {
public void accept(Integer integer, Throwable cause) {
future.complete(integer);
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.quarkus.commandmode;

import io.quarkus.runtime.QuarkusApplication;

public class ThrowExceptionApplication implements QuarkusApplication {

@Override
public int run(String... args) throws Exception {
throw new RuntimeException("Exception thrown from application");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.quarkus.commandmode;

import java.util.function.BiConsumer;

import io.quarkus.runtime.Quarkus;
import io.quarkus.runtime.annotations.QuarkusMain;

@QuarkusMain
public class ThrowExceptionApplicationMain {

public static void main(String... args) {
Quarkus.run(ThrowExceptionApplication.class, new BiConsumer<Integer, Throwable>() {
@Override
public void accept(Integer exitCode, Throwable cause) {
System.out.println("Exception and exit code [" + exitCode + "] handled by application");
System.exit(10);
}
});
}
}