From 5d98d3de93b33c9ffa4ce7151b54f81b4c4285f4 Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Mon, 9 Mar 2020 15:05:23 +1100 Subject: [PATCH] Command mode support --- bom/deployment/pom.xml | 6 - bom/runtime/pom.xml | 17 + .../quarkus/deployment/QuarkusAugmentor.java | 3 + .../RawCommandLineArgumentsBuildItem.java | 31 ++ .../deployment}/dev/ClassLoaderCompiler.java | 2 +- .../deployment}/dev/CompilationProvider.java | 2 +- .../deployment}/dev/CompilerFlags.java | 2 +- .../deployment}/dev/DevModeContext.java | 7 +- .../quarkus/deployment}/dev/DevModeMain.java | 5 +- .../dev/HotDeploymentConfigFileBuildStep.java | 2 +- .../deployment}/dev/IsolatedDevModeMain.java | 42 ++- .../dev/JavaCompilationProvider.java | 2 +- .../quarkus/deployment/dev/LauncherMain.java | 38 +++ .../dev/RuntimeUpdatesProcessor.java | 2 +- .../quarkus/deployment/pkg/PackageConfig.java | 4 +- .../pkg/steps/JarResultBuildStep.java | 35 +- .../deployment/steps/MainClassBuildStep.java | 127 ++++++-- .../runner/bootstrap/AugmentActionImpl.java | 7 +- .../runner/bootstrap/StartupActionImpl.java | 79 ++++- ...quarkus.deployment.dev.CompilationProvider | 1 + .../deployment}/dev/CompilerFlagsTest.java | 2 +- .../ApplicationStateNotification.java | 52 +++ core/devmode/pom.xml | 65 ---- .../io.quarkus.dev.CompilationProvider | 1 - core/launcher/pom.xml | 59 ++++ .../io/quarkus/launcher/QuarkusLauncher.java | 50 +++ core/pom.xml | 2 +- core/runtime/pom.xml | 14 + .../java/io/quarkus/runtime/Application.java | 142 ++------ .../runtime/ApplicationLifecycleManager.java | 308 ++++++++++++++++++ .../main/java/io/quarkus/runtime/Quarkus.java | 168 ++++++++++ .../quarkus/runtime/QuarkusApplication.java | 6 + .../io/quarkus/runtime/StartupContext.java | 18 + .../annotations/CommandLineArguments.java | 15 + .../runtime/annotations/DefaultMain.java | 29 ++ .../quarkus/commandmode/ExitCodeTestCase.java | 96 ++++++ .../quarkus/commandmode/HelloWorldMain.java | 13 + .../commandmode/HelloWorldNonDefault.java | 11 + .../java/io/quarkus/commandmode/JavaMain.java | 12 + .../quarkus/commandmode/JavaMainTestCase.java | 28 ++ .../SimpleCommandModeTestCase.java | 28 ++ devtools/gradle/build.gradle | 2 +- devtools/gradle/pom.xml | 4 +- .../io/quarkus/gradle/tasks/QuarkusDev.java | 4 +- devtools/maven/pom.xml | 8 +- .../main/java/io/quarkus/maven/DevMojo.java | 15 +- .../main/asciidoc/command-mode-reference.adoc | 84 +++++ docs/src/main/asciidoc/index.adoc | 1 + .../quarkus/arc/deployment/ArcProcessor.java | 14 + .../CommandLineArgumentsProcessor.java | 26 ++ .../CommandLineArgumentsTestCase.java | 30 ++ .../io/quarkus/arc/runtime/ArcRecorder.java | 9 + .../runtime/CommandLineArgumentsProducer.java | 24 ++ extensions/kogito/deployment/pom.xml | 4 - .../deployment/KogitoCompilationProvider.java | 2 +- ...uarkus.deployment.dev.CompilationProvider} | 0 extensions/kotlin/deployment/pom.xml | 5 - .../deployment/KotlinCompilationProvider.java | 2 +- ...uarkus.deployment.dev.CompilationProvider} | 0 extensions/scala/deployment/pom.xml | 5 - .../deployment/ScalaCompilationProvider.java | 2 +- .../SmallRyeFaultToleranceProcessor.java | 6 + .../deployment/SmallRyeMetricsProcessor.java | 6 +- .../quarkus/bootstrap/app/StartupAction.java | 15 +- .../io/quarkus/bootstrap/model/AppModel.java | 13 +- .../quarkus/it/hibernate/validator/Main.java | 10 + test-framework/junit5-internal/pom.xml | 4 - .../io/quarkus/test/QuarkusDevModeTest.java | 8 +- .../io/quarkus/test/QuarkusProdModeTest.java | 52 ++- .../java/io/quarkus/test/QuarkusUnitTest.java | 12 +- 70 files changed, 1606 insertions(+), 294 deletions(-) create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/builditem/RawCommandLineArgumentsBuildItem.java rename core/{devmode/src/main/java/io/quarkus => deployment/src/main/java/io/quarkus/deployment}/dev/ClassLoaderCompiler.java (99%) rename core/{devmode/src/main/java/io/quarkus => deployment/src/main/java/io/quarkus/deployment}/dev/CompilationProvider.java (98%) rename core/{devmode/src/main/java/io/quarkus => deployment/src/main/java/io/quarkus/deployment}/dev/CompilerFlags.java (98%) rename core/{devmode/src/main/java/io/quarkus => deployment/src/main/java/io/quarkus/deployment}/dev/DevModeContext.java (96%) rename core/{devmode/src/main/java/io/quarkus => deployment/src/main/java/io/quarkus/deployment}/dev/DevModeMain.java (96%) rename core/{devmode/src/main/java/io/quarkus => deployment/src/main/java/io/quarkus/deployment}/dev/HotDeploymentConfigFileBuildStep.java (96%) rename core/{devmode/src/main/java/io/quarkus => deployment/src/main/java/io/quarkus/deployment}/dev/IsolatedDevModeMain.java (84%) rename core/{devmode/src/main/java/io/quarkus => deployment/src/main/java/io/quarkus/deployment}/dev/JavaCompilationProvider.java (99%) create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/LauncherMain.java rename core/{devmode/src/main/java/io/quarkus => deployment/src/main/java/io/quarkus/deployment}/dev/RuntimeUpdatesProcessor.java (99%) create mode 100644 core/deployment/src/main/resources/META-INF/services/io.quarkus.deployment.dev.CompilationProvider rename core/{devmode/src/test/java/io/quarkus => deployment/src/test/java/io/quarkus/deployment}/dev/CompilerFlagsTest.java (99%) create mode 100644 core/devmode-spi/src/main/java/io/quarkus/dev/appstate/ApplicationStateNotification.java delete mode 100644 core/devmode/pom.xml delete mode 100644 core/devmode/src/main/resources/META-INF/services/io.quarkus.dev.CompilationProvider create mode 100644 core/launcher/pom.xml create mode 100644 core/launcher/src/main/java/io/quarkus/launcher/QuarkusLauncher.java create mode 100644 core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java create mode 100644 core/runtime/src/main/java/io/quarkus/runtime/Quarkus.java create mode 100644 core/runtime/src/main/java/io/quarkus/runtime/QuarkusApplication.java create mode 100644 core/runtime/src/main/java/io/quarkus/runtime/annotations/CommandLineArguments.java create mode 100644 core/runtime/src/main/java/io/quarkus/runtime/annotations/DefaultMain.java create mode 100644 core/test-extension/deployment/src/test/java/io/quarkus/commandmode/ExitCodeTestCase.java create mode 100644 core/test-extension/deployment/src/test/java/io/quarkus/commandmode/HelloWorldMain.java create mode 100644 core/test-extension/deployment/src/test/java/io/quarkus/commandmode/HelloWorldNonDefault.java create mode 100644 core/test-extension/deployment/src/test/java/io/quarkus/commandmode/JavaMain.java create mode 100644 core/test-extension/deployment/src/test/java/io/quarkus/commandmode/JavaMainTestCase.java create mode 100644 core/test-extension/deployment/src/test/java/io/quarkus/commandmode/SimpleCommandModeTestCase.java create mode 100644 docs/src/main/asciidoc/command-mode-reference.adoc create mode 100644 extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/CommandLineArgumentsProcessor.java create mode 100644 extensions/arc/deployment/src/test/java/io/quarkus/arc/test/arguments/CommandLineArgumentsTestCase.java create mode 100644 extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/CommandLineArgumentsProducer.java rename extensions/kogito/deployment/src/main/resources/META-INF/services/{io.quarkus.dev.CompilationProvider => io.quarkus.deployment.dev.CompilationProvider} (100%) rename extensions/kotlin/deployment/src/main/resources/META-INF/services/{io.quarkus.dev.CompilationProvider => io.quarkus.deployment.dev.CompilationProvider} (100%) create mode 100644 integration-tests/hibernate-validator/src/main/java/io/quarkus/it/hibernate/validator/Main.java diff --git a/bom/deployment/pom.xml b/bom/deployment/pom.xml index 5337b2c7f9e32..2c54447b4a690 100644 --- a/bom/deployment/pom.xml +++ b/bom/deployment/pom.xml @@ -617,12 +617,6 @@ ${project.version} - - io.quarkus - quarkus-development-mode - ${project.version} - - io.quarkus quarkus-scala-deployment diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml index 0b4bdf88aa966..4c0bcc90d56a9 100644 --- a/bom/runtime/pom.xml +++ b/bom/runtime/pom.xml @@ -280,6 +280,23 @@ quarkus-development-mode-spi ${project.version} + + io.quarkus + quarkus-ide-launcher + ${project.version} + + + io.quarkus + quarkus-ide-launcher + ${project.version} + + + + * + * + + + io.quarkus quarkus-arc diff --git a/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java b/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java index 2431614c067b7..dc0f2f2b1823b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java @@ -31,6 +31,7 @@ import io.quarkus.deployment.builditem.GeneratedResourceBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.LiveReloadBuildItem; +import io.quarkus.deployment.builditem.RawCommandLineArgumentsBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; @@ -99,6 +100,7 @@ public BuildResult run() throws Exception { .addInitial(DeploymentClassLoaderBuildItem.class) .addInitial(ArchiveRootBuildItem.class) .addInitial(ShutdownContextBuildItem.class) + .addInitial(RawCommandLineArgumentsBuildItem.class) .addInitial(LaunchModeBuildItem.class) .addInitial(LiveReloadBuildItem.class) .addInitial(AdditionalApplicationArchiveBuildItem.class) @@ -124,6 +126,7 @@ public BuildResult run() throws Exception { .produce(liveReloadBuildItem) .produce(new ArchiveRootBuildItem(root, rootFs == null ? root : rootFs.getPath("/"), excludedFromIndexing)) .produce(new ShutdownContextBuildItem()) + .produce(new RawCommandLineArgumentsBuildItem()) .produce(new LaunchModeBuildItem(launchMode)) .produce(new BuildSystemTargetBuildItem(targetDir, baseName)) .produce(new DeploymentClassLoaderBuildItem(deploymentClassLoader)) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/RawCommandLineArgumentsBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/RawCommandLineArgumentsBuildItem.java new file mode 100644 index 0000000000000..2b51d86154bd5 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/RawCommandLineArgumentsBuildItem.java @@ -0,0 +1,31 @@ +package io.quarkus.deployment.builditem; + +import java.util.function.Supplier; + +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.deployment.recording.BytecodeRecorderImpl; +import io.quarkus.runtime.StartupContext; + +/** + * A build item that represents the raw command line arguments as they were passed to the application. + * + * No filtering is done on these parameters. + */ +public final class RawCommandLineArgumentsBuildItem extends SimpleBuildItem + implements BytecodeRecorderImpl.ReturnedProxy, Supplier { + + @Override + public String __returned$proxy$key() { + return StartupContext.RAW_COMMAND_LINE_ARGS; + } + + @Override + public boolean __static$$init() { + return true; + } + + @Override + public String[] get() { + throw new IllegalStateException("Can only be called at runtime"); + } +} diff --git a/core/devmode/src/main/java/io/quarkus/dev/ClassLoaderCompiler.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/ClassLoaderCompiler.java similarity index 99% rename from core/devmode/src/main/java/io/quarkus/dev/ClassLoaderCompiler.java rename to core/deployment/src/main/java/io/quarkus/deployment/dev/ClassLoaderCompiler.java index 9c57f61f483a8..77391bc2d59d8 100644 --- a/core/devmode/src/main/java/io/quarkus/dev/ClassLoaderCompiler.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/ClassLoaderCompiler.java @@ -1,4 +1,4 @@ -package io.quarkus.dev; +package io.quarkus.deployment.dev; import java.io.File; import java.io.IOException; diff --git a/core/devmode/src/main/java/io/quarkus/dev/CompilationProvider.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/CompilationProvider.java similarity index 98% rename from core/devmode/src/main/java/io/quarkus/dev/CompilationProvider.java rename to core/deployment/src/main/java/io/quarkus/deployment/dev/CompilationProvider.java index 253494593b6e8..3514b039ccdd7 100644 --- a/core/devmode/src/main/java/io/quarkus/dev/CompilationProvider.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/CompilationProvider.java @@ -1,4 +1,4 @@ -package io.quarkus.dev; +package io.quarkus.deployment.dev; import java.io.File; import java.nio.charset.Charset; diff --git a/core/devmode/src/main/java/io/quarkus/dev/CompilerFlags.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/CompilerFlags.java similarity index 98% rename from core/devmode/src/main/java/io/quarkus/dev/CompilerFlags.java rename to core/deployment/src/main/java/io/quarkus/deployment/dev/CompilerFlags.java index 6d26b999eb073..2cdab4f1b9d66 100644 --- a/core/devmode/src/main/java/io/quarkus/dev/CompilerFlags.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/CompilerFlags.java @@ -1,4 +1,4 @@ -package io.quarkus.dev; +package io.quarkus.deployment.dev; import java.util.ArrayList; import java.util.Collection; diff --git a/core/devmode/src/main/java/io/quarkus/dev/DevModeContext.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeContext.java similarity index 96% rename from core/devmode/src/main/java/io/quarkus/dev/DevModeContext.java rename to core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeContext.java index e491947f88761..08023a16c9f17 100644 --- a/core/devmode/src/main/java/io/quarkus/dev/DevModeContext.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeContext.java @@ -1,4 +1,4 @@ -package io.quarkus.dev; +package io.quarkus.deployment.dev; import java.io.File; import java.io.Serializable; @@ -27,6 +27,7 @@ public class DevModeContext implements Serializable { private String sourceEncoding; private final List classesRoots = new ArrayList<>(); + private final List additionalClassPathElements = new ArrayList<>(); private File frameworkClassesDir; private File cacheDir; private File projectDir; @@ -80,6 +81,10 @@ public List getClassesRoots() { return classesRoots; } + public List getAdditionalClassPathElements() { + return additionalClassPathElements; + } + public File getFrameworkClassesDir() { return frameworkClassesDir; } diff --git a/core/devmode/src/main/java/io/quarkus/dev/DevModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java similarity index 96% rename from core/devmode/src/main/java/io/quarkus/dev/DevModeMain.java rename to core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java index b66e3ef9fb0ff..39f184ed7e8ff 100644 --- a/core/devmode/src/main/java/io/quarkus/dev/DevModeMain.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java @@ -1,4 +1,5 @@ -package io.quarkus.dev; + +package io.quarkus.deployment.dev; import java.io.Closeable; import java.io.DataInputStream; @@ -20,6 +21,7 @@ import io.quarkus.bootstrap.app.AdditionalDependency; import io.quarkus.bootstrap.app.CuratedApplication; import io.quarkus.bootstrap.app.QuarkusBootstrap; +import io.quarkus.dev.appstate.ApplicationStateNotification; /** * The main entry point for the dev mojo execution @@ -115,5 +117,6 @@ public void close() throws IOException { if (realCloseable != null) { realCloseable.close(); } + ApplicationStateNotification.waitForApplicationStop(); } } diff --git a/core/devmode/src/main/java/io/quarkus/dev/HotDeploymentConfigFileBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/HotDeploymentConfigFileBuildStep.java similarity index 96% rename from core/devmode/src/main/java/io/quarkus/dev/HotDeploymentConfigFileBuildStep.java rename to core/deployment/src/main/java/io/quarkus/deployment/dev/HotDeploymentConfigFileBuildStep.java index 6f72aa76ff6eb..e227702fecd71 100644 --- a/core/devmode/src/main/java/io/quarkus/dev/HotDeploymentConfigFileBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/HotDeploymentConfigFileBuildStep.java @@ -1,4 +1,4 @@ -package io.quarkus.dev; +package io.quarkus.deployment.dev; import java.util.List; import java.util.Map; diff --git a/core/devmode/src/main/java/io/quarkus/dev/IsolatedDevModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java similarity index 84% rename from core/devmode/src/main/java/io/quarkus/dev/IsolatedDevModeMain.java rename to core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java index ce921f073d0a6..85c0bb04f3f6a 100644 --- a/core/devmode/src/main/java/io/quarkus/dev/IsolatedDevModeMain.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java @@ -1,4 +1,4 @@ -package io.quarkus.dev; +package io.quarkus.deployment.dev; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -29,8 +29,10 @@ import io.quarkus.builder.BuildContext; import io.quarkus.builder.BuildStep; import io.quarkus.deployment.builditem.ApplicationClassPredicateBuildItem; +import io.quarkus.dev.appstate.ApplicationStateNotification; import io.quarkus.dev.spi.HotReplacementSetup; import io.quarkus.runner.bootstrap.AugmentActionImpl; +import io.quarkus.runtime.ApplicationLifecycleManager; import io.quarkus.runtime.Timing; import io.quarkus.runtime.configuration.QuarkusConfigFactory; import io.quarkus.runtime.logging.InitialConfigurator; @@ -49,6 +51,7 @@ public class IsolatedDevModeMain implements BiConsumer() { + @Override + public void accept(Integer integer) { + if (restarting || ApplicationLifecycleManager.isVmShuttingDown()) { + return; + } + System.out.println("Quarkus application exited with code " + integer); + System.out.println("Press Enter to restart"); + try { + while (System.in.read() != '\n') { + } + runtimeUpdatesProcessor.checkForChangedClasses(); + restartApp(runtimeUpdatesProcessor.checkForFileChange()); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + + runner = start.runMainClass(); } catch (Throwable t) { deploymentProblem = t; if (context.isAbortOnFailedStart()) { @@ -91,7 +118,12 @@ private synchronized void firstStart() { } public synchronized void restartApp(Set changedResources) { + restarting = true; stop(); + + //this clears any old state + ApplicationStateNotification.notifyApplicationStopped(); + restarting = false; Timing.restart(curatedApplication.getAugmentClassLoader()); deploymentProblem = null; ClassLoader old = Thread.currentThread().getContextClassLoader(); @@ -100,11 +132,11 @@ public synchronized void restartApp(Set changedResources) { //ok, we have resolved all the deps try { StartupAction start = augmentAction.reloadExistingApplication(changedResources); - runner = start.run(); + runner = start.runMainClass(); + ApplicationStateNotification.waitForApplicationStart(); } catch (Throwable t) { deploymentProblem = t; log.error("Failed to start quarkus", t); - } } finally { Thread.currentThread().setContextClassLoader(old); @@ -169,6 +201,8 @@ public void stop() { } public void close() { + //don't attempt to restart in the exit code handler + restarting = true; try { stop(); } finally { diff --git a/core/devmode/src/main/java/io/quarkus/dev/JavaCompilationProvider.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/JavaCompilationProvider.java similarity index 99% rename from core/devmode/src/main/java/io/quarkus/dev/JavaCompilationProvider.java rename to core/deployment/src/main/java/io/quarkus/deployment/dev/JavaCompilationProvider.java index 40153c61affe9..6e8ebb4210e12 100644 --- a/core/devmode/src/main/java/io/quarkus/dev/JavaCompilationProvider.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/JavaCompilationProvider.java @@ -1,4 +1,4 @@ -package io.quarkus.dev; +package io.quarkus.deployment.dev; import java.io.File; import java.io.IOException; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/LauncherMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/LauncherMain.java new file mode 100644 index 0000000000000..09462f6abe909 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/LauncherMain.java @@ -0,0 +1,38 @@ +package io.quarkus.deployment.dev; + +import java.io.File; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; + +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; + +import io.smallrye.config.SmallRyeConfigProviderResolver; + +/** + * The entry point when launched from an IDE + */ +public class LauncherMain { + + public static void main(Path appClasses, Path wiring, URL[] classPath, String... args) throws Exception { + DevModeContext context = new DevModeContext(); + context.setAbortOnFailedStart(true); + context.setTest(false); + context.setCacheDir(Files.createTempDirectory("quarkus-cache").toFile()); + context.setSourceEncoding("UTF-8"); + File appClassesFile = appClasses.toFile(); + context.getClassesRoots().add(appClassesFile); + + //TODO: huge hacks + File src = new File(appClassesFile, "../../src/main/java"); + File res = new File(appClassesFile, "../../src/main/resources"); + + context.getModules().add(new DevModeContext.ModuleInfo("main", new File("").getAbsolutePath(), + Collections.singleton(src.getAbsolutePath()), appClassesFile.getAbsolutePath(), res.getAbsolutePath())); + //the loading of this is super wierd, and does its own class loader delegation for some reason + ConfigProviderResolver.setInstance(new SmallRyeConfigProviderResolver()); + DevModeMain main = new DevModeMain(context); + main.start(); + } +} diff --git a/core/devmode/src/main/java/io/quarkus/dev/RuntimeUpdatesProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java similarity index 99% rename from core/devmode/src/main/java/io/quarkus/dev/RuntimeUpdatesProcessor.java rename to core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java index fa77d6a2c380a..7e1b841b9beab 100644 --- a/core/devmode/src/main/java/io/quarkus/dev/RuntimeUpdatesProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java @@ -1,4 +1,4 @@ -package io.quarkus.dev; +package io.quarkus.deployment.dev; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.toList; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java index 9efe7e622399b..59544bbf05bcb 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java @@ -35,8 +35,8 @@ public class PackageConfig { /** * The entry point of the application. In most cases this should not be modified. */ - @ConfigItem(defaultValue = "io.quarkus.runner.GeneratedMain") - public String mainClass; + @ConfigItem + public Optional mainClass; /** * Files that should not be copied to the output artifact diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java index af55e6b87f2c6..886d73461f236 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java @@ -57,6 +57,7 @@ import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.GeneratedNativeImageClassBuildItem; import io.quarkus.deployment.builditem.GeneratedResourceBuildItem; +import io.quarkus.deployment.builditem.MainClassBuildItem; import io.quarkus.deployment.builditem.TransformedClassesBuildItem; import io.quarkus.deployment.pkg.PackageConfig; import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; @@ -144,13 +145,15 @@ public JarBuildItem buildRunnerJar(CurateOutcomeBuildItem curateOutcomeBuildItem PackageConfig packageConfig, List generatedClasses, List generatedResources, - List uberJarRequired) throws Exception { + List uberJarRequired, + MainClassBuildItem mainClassBuildItem) throws Exception { if (!uberJarRequired.isEmpty() || packageConfig.uberJar) { return buildUberJar(curateOutcomeBuildItem, outputTargetBuildItem, transformedClasses, applicationArchivesBuildItem, - packageConfig, applicationInfo, generatedClasses, generatedResources); + packageConfig, applicationInfo, generatedClasses, generatedResources, + mainClassBuildItem); } else { return buildThinJar(curateOutcomeBuildItem, outputTargetBuildItem, transformedClasses, applicationArchivesBuildItem, - packageConfig, applicationInfo, generatedClasses, generatedResources); + packageConfig, applicationInfo, generatedClasses, generatedResources, mainClassBuildItem); } } @@ -161,7 +164,8 @@ private JarBuildItem buildUberJar(CurateOutcomeBuildItem curateOutcomeBuildItem, PackageConfig packageConfig, ApplicationInfoBuildItem applicationInfo, List generatedClasses, - List generatedResources) throws Exception { + List generatedResources, + MainClassBuildItem mainClassBuildItem) throws Exception { //for uberjars we move the original jar, so there is only a single jar in the output directory Path standardJar = outputTargetBuildItem.getOutputDirectory().resolve(outputTargetBuildItem.getBaseName() + ".jar"); @@ -194,7 +198,8 @@ private JarBuildItem buildUberJar(CurateOutcomeBuildItem curateOutcomeBuildItem, AppArtifact appArtifact = curateOutcomeBuildItem.getEffectiveModel().getAppArtifact(); // the manifest needs to be the first entry in the jar, otherwise JarInputStream does not work properly // see https://bugs.openjdk.java.net/browse/JDK-8031748 - generateManifest(runnerZipFs, classPath.toString(), packageConfig, appArtifact, applicationInfo); + generateManifest(runnerZipFs, classPath.toString(), packageConfig, appArtifact, mainClassBuildItem, + applicationInfo); for (AppDependency appDep : appDeps) { final AppArtifact depArtifact = appDep.getArtifact(); @@ -310,7 +315,8 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, PackageConfig packageConfig, ApplicationInfoBuildItem applicationInfo, List generatedClasses, - List generatedResources) throws Exception { + List generatedResources, + MainClassBuildItem mainClassBuildItem) throws Exception { Path runnerJar = outputTargetBuildItem.getOutputDirectory() .resolve(outputTargetBuildItem.getBaseName() + packageConfig.runnerSuffix + ".jar"); @@ -324,7 +330,7 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, log.info("Building thin jar: " + runnerJar); doThinJarGeneration(curateOutcomeBuildItem, transformedClasses, applicationArchivesBuildItem, applicationInfo, - packageConfig, generatedResources, libDir, generatedClasses, runnerZipFs); + packageConfig, generatedResources, libDir, generatedClasses, runnerZipFs, mainClassBuildItem); } runnerJar.toFile().setReadable(true, false); @@ -344,7 +350,8 @@ public NativeImageSourceJarBuildItem buildNativeImageJar(CurateOutcomeBuildItem PackageConfig packageConfig, List generatedClasses, List nativeImageResources, - List generatedResources) throws Exception { + List generatedResources, + MainClassBuildItem mainClassBuildItem) throws Exception { Path thinJarDirectory = outputTargetBuildItem.getOutputDirectory() .resolve(outputTargetBuildItem.getBaseName() + "-native-image-source-jar"); IoUtils.recursiveDelete(thinJarDirectory); @@ -365,7 +372,7 @@ public NativeImageSourceJarBuildItem buildNativeImageJar(CurateOutcomeBuildItem log.info("Building native image source jar: " + runnerJar); doThinJarGeneration(curateOutcomeBuildItem, transformedClasses, applicationArchivesBuildItem, applicationInfo, - packageConfig, generatedResources, libDir, allClasses, runnerZipFs); + packageConfig, generatedResources, libDir, allClasses, runnerZipFs, mainClassBuildItem); } runnerJar.toFile().setReadable(true, false); return new NativeImageSourceJarBuildItem(runnerJar, libDir); @@ -426,7 +433,8 @@ private void doThinJarGeneration(CurateOutcomeBuildItem curateOutcomeBuildItem, List generatedResources, Path libDir, List allClasses, - FileSystem runnerZipFs) + FileSystem runnerZipFs, + MainClassBuildItem mainClassBuildItem) throws BootstrapDependencyProcessingException, AppModelResolverException, IOException { final Map seen = new HashMap<>(); final StringBuilder classPath = new StringBuilder(); @@ -440,7 +448,7 @@ private void doThinJarGeneration(CurateOutcomeBuildItem curateOutcomeBuildItem, AppArtifact appArtifact = curateOutcomeBuildItem.getEffectiveModel().getAppArtifact(); // the manifest needs to be the first entry in the jar, otherwise JarInputStream does not work properly // see https://bugs.openjdk.java.net/browse/JDK-8031748 - generateManifest(runnerZipFs, classPath.toString(), packageConfig, appArtifact, applicationInfo); + generateManifest(runnerZipFs, classPath.toString(), packageConfig, appArtifact, mainClassBuildItem, applicationInfo); copyCommonContent(runnerZipFs, services, applicationArchivesBuildItem, transformedClasses, allClasses, generatedResources, seen); } @@ -606,6 +614,7 @@ private void filterZipFile(Path resolvedDep, Path targetPath, Set transf * Otherwise this manifest manipulation will be useless. */ private void generateManifest(FileSystem runnerZipFs, final String classPath, PackageConfig config, AppArtifact appArtifact, + MainClassBuildItem mainClassBuildItem, ApplicationInfoBuildItem applicationInfo) throws IOException { final Path manifestPath = runnerZipFs.getPath("META-INF", "MANIFEST.MF"); @@ -628,11 +637,11 @@ private void generateManifest(FileSystem runnerZipFs, final String classPath, Pa attributes.put(Attributes.Name.CLASS_PATH, classPath); if (attributes.containsKey(Attributes.Name.MAIN_CLASS)) { String existingMainClass = attributes.getValue(Attributes.Name.MAIN_CLASS); - if (!config.mainClass.equals(existingMainClass)) { + if (!mainClassBuildItem.getClassName().equals(existingMainClass)) { log.warn("Your MANIFEST.MF already defined a MAIN_CLASS entry. Quarkus has overwritten your existing entry."); } } - attributes.put(Attributes.Name.MAIN_CLASS, config.mainClass); + attributes.put(Attributes.Name.MAIN_CLASS, mainClassBuildItem.getClassName()); if (config.manifest.addImplementationEntries && !attributes.containsKey(Attributes.Name.IMPLEMENTATION_TITLE)) { String name = ApplicationInfoBuildItem.UNSET_VALUE.equals(applicationInfo.getName()) ? appArtifact.getArtifactId() : applicationInfo.getName(); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java index 472933b724c58..1e81897da9378 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java @@ -5,20 +5,26 @@ import java.io.File; import java.lang.reflect.Modifier; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import org.graalvm.nativeimage.ImageInfo; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; import org.jboss.logging.Logger; import io.quarkus.builder.Version; import io.quarkus.deployment.GeneratedClassGizmoAdaptor; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; import io.quarkus.deployment.builditem.ApplicationClassNameBuildItem; import io.quarkus.deployment.builditem.ApplicationInfoBuildItem; import io.quarkus.deployment.builditem.BytecodeRecorderObjectLoaderBuildItem; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.JavaLibraryPathAdditionalPathBuildItem; @@ -29,7 +35,9 @@ import io.quarkus.deployment.builditem.SslTrustStoreSystemPropertyBuildItem; import io.quarkus.deployment.builditem.StaticBytecodeRecorderBuildItem; import io.quarkus.deployment.builditem.SystemPropertyBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.configuration.RunTimeConfigurationGenerator; +import io.quarkus.deployment.pkg.PackageConfig; import io.quarkus.deployment.recording.BytecodeRecorderImpl; import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.CatchBlockCreator; @@ -43,25 +51,27 @@ import io.quarkus.runtime.Application; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.NativeImageRuntimePropertiesRecorder; +import io.quarkus.runtime.Quarkus; +import io.quarkus.runtime.QuarkusApplication; import io.quarkus.runtime.StartupContext; import io.quarkus.runtime.StartupTask; import io.quarkus.runtime.Timing; +import io.quarkus.runtime.annotations.DefaultMain; import io.quarkus.runtime.configuration.ProfileManager; class MainClassBuildStep { - private static final String APP_CLASS = "io.quarkus.runner.ApplicationImpl"; - private static final String MAIN_CLASS = "io.quarkus.runner.GeneratedMain"; - private static final String STARTUP_CONTEXT = "STARTUP_CONTEXT"; - private static final String LOG = "LOG"; - private static final String JAVA_LIBRARY_PATH = "java.library.path"; - private static final String JAVAX_NET_SSL_TRUST_STORE = "javax.net.ssl.trustStore"; + static final String MAIN_CLASS = "io.quarkus.runner.GeneratedMain"; + static final String STARTUP_CONTEXT = "STARTUP_CONTEXT"; + static final String LOG = "LOG"; + static final String JAVA_LIBRARY_PATH = "java.library.path"; + static final String JAVAX_NET_SSL_TRUST_STORE = "javax.net.ssl.trustStore"; - private static final FieldDescriptor STARTUP_CONTEXT_FIELD = FieldDescriptor.of(APP_CLASS, STARTUP_CONTEXT, + private static final FieldDescriptor STARTUP_CONTEXT_FIELD = FieldDescriptor.of(Application.APP_CLASS_NAME, STARTUP_CONTEXT, StartupContext.class); @BuildStep - MainClassBuildItem build(List staticInitTasks, + void build(List staticInitTasks, List substitutions, List mainMethod, List properties, @@ -74,11 +84,11 @@ MainClassBuildItem build(List staticInitTasks, LaunchModeBuildItem launchMode, ApplicationInfoBuildItem applicationInfo) { - appClassNameProducer.produce(new ApplicationClassNameBuildItem(APP_CLASS)); + appClassNameProducer.produce(new ApplicationClassNameBuildItem(Application.APP_CLASS_NAME)); // Application class GeneratedClassGizmoAdaptor gizmoOutput = new GeneratedClassGizmoAdaptor(generatedClass, true); - ClassCreator file = new ClassCreator(gizmoOutput, APP_CLASS, null, + ClassCreator file = new ClassCreator(gizmoOutput, Application.APP_CLASS_NAME, null, Application.class.getName()); // Application class: static init @@ -176,6 +186,11 @@ MainClassBuildItem build(List staticInitTasks, mv.invokeStaticMethod(ofMethod(Timing.class, "mainStarted", void.class)); startupContext = mv.readStaticField(scField.getFieldDescriptor()); + //now set the command line arguments + mv.invokeVirtualMethod( + MethodDescriptor.ofMethod(StartupContext.class, "setCommandLineArguments", void.class, String[].class), + startupContext, mv.getMethodParam(0)); + tryBlock = mv.tryBlock(); for (MainBytecodeRecorderBuildItem holder : mainMethod) { @@ -216,31 +231,72 @@ MainClassBuildItem build(List staticInitTasks, mv.invokeVirtualMethod(ofMethod(StartupContext.class, "close", void.class), startupContext); mv.returnValue(null); + // getName method + mv = file.getMethodCreator("getName", String.class); + mv.returnValue(mv.load(applicationInfo.getName())); + // Finish application class file.close(); + } - // Main class - - file = new ClassCreator(gizmoOutput, MAIN_CLASS, null, - Object.class.getName()); - - mv = file.getMethodCreator("main", void.class, String[].class); - mv.setModifiers(Modifier.PUBLIC | Modifier.STATIC); - - final ResultHandle appClassInstance = mv.newInstance(ofConstructor(APP_CLASS)); - - // Set the application name - mv.invokeVirtualMethod(ofMethod(Application.class, "setName", void.class, String.class), appClassInstance, - mv.load(applicationInfo.getName())); - - // run the app - mv.invokeVirtualMethod(ofMethod(Application.class, "run", void.class, String[].class), appClassInstance, - mv.getMethodParam(0)); - - mv.returnValue(null); + @BuildStep + public MainClassBuildItem mainClassBuildStep(BuildProducer generatedClass, + ApplicationArchivesBuildItem applicationArchivesBuildItem, + CombinedIndexBuildItem combinedIndexBuildItem, + PackageConfig packageConfig) { + String mainClassName = MAIN_CLASS; + if (packageConfig.mainClass.isPresent()) { + mainClassName = packageConfig.mainClass.get(); + } else { + Collection defaultMains = applicationArchivesBuildItem.getRootArchive().getIndex() + .getAnnotations(DotName.createSimple(DefaultMain.class.getName())); + if (defaultMains.size() > 1) { + throw new RuntimeException( + "More than one @DefaultMain method found in application, could not determine main class to use" + + defaultMains); + } else if (!defaultMains.isEmpty()) { + mainClassName = defaultMains.iterator().next().target().asClass().name().toString(); + } + } + if (mainClassName.equals(MAIN_CLASS)) { + //generate a main that just runs the app, the user has not supplied a main class + ClassCreator file = new ClassCreator(new GeneratedClassGizmoAdaptor(generatedClass, true), MAIN_CLASS, null, + Object.class.getName()); + + MethodCreator mv = file.getMethodCreator("main", void.class, String[].class); + mv.setModifiers(Modifier.PUBLIC | Modifier.STATIC); + mv.invokeStaticMethod(MethodDescriptor.ofMethod(Quarkus.class, "run", void.class, String[].class), + mv.getMethodParam(0)); + mv.returnValue(null); + + file.close(); + } else { + Collection impls = combinedIndexBuildItem.getIndex() + .getAllKnownImplementors(DotName.createSimple(QuarkusApplication.class.getName())); + boolean found = false; + for (ClassInfo i : impls) { + if (i.name().toString().equals(mainClassName)) { + found = true; + break; + } + } + if (found) { + //this is QuarkusApplication, generate a real main to run it + ClassCreator file = new ClassCreator(new GeneratedClassGizmoAdaptor(generatedClass, true), MAIN_CLASS, null, + Object.class.getName()); + + MethodCreator mv = file.getMethodCreator("main", void.class, String[].class); + mv.setModifiers(Modifier.PUBLIC | Modifier.STATIC); + mv.invokeStaticMethod(MethodDescriptor.ofMethod(Quarkus.class, "run", void.class, Class.class, String[].class), + mv.loadClass(mainClassName), + mv.getMethodParam(0)); + mv.returnValue(null); + file.close(); + mainClassName = MAIN_CLASS; + } + } - file.close(); - return new MainClassBuildItem(MAIN_CLASS); + return new MainClassBuildItem(mainClassName); } private void writeRecordedBytecode(BytecodeRecorderImpl recorder, String fallbackGeneratedStartupTaskClassName, @@ -269,4 +325,13 @@ private void writeRecordedBytecode(BytecodeRecorderImpl recorder, String fallbac startupContext); } + /** + * registers the generated application class for reflection, needed when launching via the Quarkus launcher + * + */ + @BuildStep + ReflectiveClassBuildItem applicationReflection() { + return new ReflectiveClassBuildItem(false, false, Application.APP_CLASS_NAME); + } + } diff --git a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java index 0f98984bfea9c..32e9df5574393 100644 --- a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java +++ b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java @@ -32,6 +32,8 @@ import io.quarkus.deployment.builditem.GeneratedResourceBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.LiveReloadBuildItem; +import io.quarkus.deployment.builditem.MainClassBuildItem; +import io.quarkus.deployment.builditem.RawCommandLineArgumentsBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; import io.quarkus.deployment.pkg.builditem.JarBuildItem; @@ -93,7 +95,8 @@ public StartupActionImpl createInitialRuntimeApplication() { throw new IllegalStateException("Cannot launch a runtime application with NORMAL launch mode"); } BuildResult result = runAugment(true, Collections.emptySet(), GeneratedClassBuildItem.class, - GeneratedResourceBuildItem.class, BytecodeTransformerBuildItem.class, ApplicationClassNameBuildItem.class); + GeneratedResourceBuildItem.class, BytecodeTransformerBuildItem.class, ApplicationClassNameBuildItem.class, + MainClassBuildItem.class); return new StartupActionImpl(curatedApplication, result); } @@ -134,6 +137,7 @@ public BuildResult runCustomAction(Consumer chainBuild, Consu .addInitial(ShutdownContextBuildItem.class) .addInitial(LaunchModeBuildItem.class) .addInitial(LiveReloadBuildItem.class) + .addInitial(RawCommandLineArgumentsBuildItem.class) .addFinal(ConfigDescriptionBuildItem.class); chainBuild.accept(chainBuilder); @@ -142,6 +146,7 @@ public BuildResult runCustomAction(Consumer chainBuild, Consu BuildExecutionBuilder execBuilder = chain.createExecutionBuilder("main") .produce(new LaunchModeBuildItem(launchMode)) .produce(new ShutdownContextBuildItem()) + .produce(new RawCommandLineArgumentsBuildItem()) .produce(new LiveReloadBuildItem()); executionBuild.accept(execBuilder); return execBuilder 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 35f74ffad8ffd..d6d690d4d3f27 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 @@ -11,6 +11,7 @@ import java.util.List; import java.util.Map; import java.util.function.BiFunction; +import java.util.function.Consumer; import org.jboss.logging.Logger; import org.objectweb.asm.ClassVisitor; @@ -26,7 +27,9 @@ import io.quarkus.deployment.builditem.DeploymentClassLoaderBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.GeneratedResourceBuildItem; +import io.quarkus.deployment.builditem.MainClassBuildItem; import io.quarkus.deployment.configuration.RunTimeConfigurationGenerator; +import io.quarkus.runtime.Quarkus; public class StartupActionImpl implements StartupAction { @@ -43,10 +46,73 @@ public StartupActionImpl(CuratedApplication curatedApplication, BuildResult buil } /** - * Runs the application, and returns a handle that can be used to shut it down. + * Runs the application by running the main method of the main class. As this is a blocking method a new + * thread is created to run this task. + * + * Before this method is called an appropriate exit handler will likely need to + * be set in {@link io.quarkus.runtime.ApplicationLifecycleManager#setDefaultExitCodeHandler(Consumer)} + * of the JVM will exit when the app stops. */ - public RunningQuarkusApplication run(String... args) throws Exception { + public RunningQuarkusApplication runMainClass(String... args) throws Exception { //first + QuarkusClassLoader runtimeClassLoader = createRuntimeClassLoader(); + + //we have our class loaders + ClassLoader old = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(runtimeClassLoader); + final String className = buildResult.consume(MainClassBuildItem.class).getClassName(); + try { + // force init here + Class appClass = Class.forName(className, true, runtimeClassLoader); + Method start = appClass.getMethod("main", String[].class); + Thread t = new Thread(new Runnable() { + @Override + public void run() { + Thread.currentThread().setContextClassLoader(runtimeClassLoader); + try { + start.invoke(null, (Object) args); + } catch (Throwable e) { + log.error("Error running Quarkus", e); + } + } + }, "Quarkus Main Thread"); + t.start(); + return new RunningQuarkusApplicationImpl(new Closeable() { + @Override + public void close() throws IOException { + try { + runtimeClassLoader.loadClass(Quarkus.class.getName()).getMethod("blockingExit").invoke(null); + } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException + | ClassNotFoundException e) { + log.error("Failed to stop Quarkus", e); + } finally { + ForkJoinClassLoading.setForkJoinClassLoader(ClassLoader.getSystemClassLoader()); + if (curatedApplication.getQuarkusBootstrap().getMode() == QuarkusBootstrap.Mode.TEST) { + //for tests we just always shut down the curated application, as it is only used once + //dev mode might be about to restart, so we leave it + curatedApplication.close(); + } + } + } + }, runtimeClassLoader); + } catch (Throwable t) { + // todo: dev mode expects run time config to be available immediately even if static init didn't complete. + try { + final Class configClass = Class.forName(RunTimeConfigurationGenerator.CONFIG_CLASS_NAME, true, + runtimeClassLoader); + configClass.getDeclaredMethod(RunTimeConfigurationGenerator.C_CREATE_BOOTSTRAP_CONFIG.getName()) + .invoke(null); + } catch (Throwable t2) { + t.addSuppressed(t2); + } + throw t; + } finally { + Thread.currentThread().setContextClassLoader(old); + } + + } + + private QuarkusClassLoader createRuntimeClassLoader() { Map>> bytecodeTransformers = extractTransformers(); QuarkusClassLoader baseClassLoader = curatedApplication.getBaseRuntimeClassLoader(); ClassLoader transformerClassLoader = buildResult.consume(DeploymentClassLoaderBuildItem.class).getClassLoader(); @@ -68,6 +134,15 @@ public RunningQuarkusApplication run(String... args) throws Exception { runtimeClassLoader = baseClassLoader; } ForkJoinClassLoading.setForkJoinClassLoader(runtimeClassLoader); + return runtimeClassLoader; + } + + /** + * Runs the application, and returns a handle that can be used to shut it down. + */ + public RunningQuarkusApplication run(String... args) throws Exception { + //first + QuarkusClassLoader runtimeClassLoader = createRuntimeClassLoader(); //we have our class loaders ClassLoader old = Thread.currentThread().getContextClassLoader(); diff --git a/core/deployment/src/main/resources/META-INF/services/io.quarkus.deployment.dev.CompilationProvider b/core/deployment/src/main/resources/META-INF/services/io.quarkus.deployment.dev.CompilationProvider new file mode 100644 index 0000000000000..2fc24c15b30d0 --- /dev/null +++ b/core/deployment/src/main/resources/META-INF/services/io.quarkus.deployment.dev.CompilationProvider @@ -0,0 +1 @@ +io.quarkus.deployment.dev.JavaCompilationProvider \ No newline at end of file diff --git a/core/devmode/src/test/java/io/quarkus/dev/CompilerFlagsTest.java b/core/deployment/src/test/java/io/quarkus/deployment/dev/CompilerFlagsTest.java similarity index 99% rename from core/devmode/src/test/java/io/quarkus/dev/CompilerFlagsTest.java rename to core/deployment/src/test/java/io/quarkus/deployment/dev/CompilerFlagsTest.java index dcd1c83f7f964..e2619a773df4e 100644 --- a/core/devmode/src/test/java/io/quarkus/dev/CompilerFlagsTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/dev/CompilerFlagsTest.java @@ -1,4 +1,4 @@ -package io.quarkus.dev; +package io.quarkus.deployment.dev; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/core/devmode-spi/src/main/java/io/quarkus/dev/appstate/ApplicationStateNotification.java b/core/devmode-spi/src/main/java/io/quarkus/dev/appstate/ApplicationStateNotification.java new file mode 100644 index 0000000000000..d5c4d38baa86d --- /dev/null +++ b/core/devmode-spi/src/main/java/io/quarkus/dev/appstate/ApplicationStateNotification.java @@ -0,0 +1,52 @@ +package io.quarkus.dev.appstate; + +/** + * A class that allows for access to the application state, even from outside the runtime class loader. + * + * This should generally only be used by dev mode internals that need information about the current + * application state. + * + * This class makes not attempt to verify that an application is starting/stopping when the + * wait methods are called, this should only be called by a client that is controlling the Quarkus + * lifecycle, and so knows what the current lifecycle state is. + */ +public class ApplicationStateNotification { + + private static boolean started = false; + private static Throwable startupProblem; + + public static synchronized void notifyStartupComplete(Throwable sp) { + started = startupProblem == null; + startupProblem = sp; + ApplicationStateNotification.class.notifyAll(); + } + + public static synchronized void notifyApplicationStopped() { + started = false; + startupProblem = null; + ApplicationStateNotification.class.notifyAll(); + } + + public static synchronized void waitForApplicationStart() { + while (!started && startupProblem == null) { + try { + ApplicationStateNotification.class.wait(); + } catch (InterruptedException e) { + //ignore + } + } + if (startupProblem != null) { + throw new RuntimeException(startupProblem); + } + } + + public static synchronized void waitForApplicationStop() { + while (started) { + try { + ApplicationStateNotification.class.wait(); + } catch (InterruptedException e) { + //ignore + } + } + } +} diff --git a/core/devmode/pom.xml b/core/devmode/pom.xml deleted file mode 100644 index 2b6bbd6ca895e..0000000000000 --- a/core/devmode/pom.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - quarkus-build-parent - io.quarkus - 999-SNAPSHOT - ../../build-parent/pom.xml - - 4.0.0 - - quarkus-development-mode - Quarkus - Development mode - - - - io.quarkus - quarkus-core-deployment - - - io.quarkus - quarkus-development-mode-spi - - - org.jboss.logmanager - jboss-logmanager-embedded - - - - - org.junit.jupiter - junit-jupiter-api - test - - - org.junit.jupiter - junit-jupiter-params - test - - - org.junit.jupiter - junit-jupiter-engine - test - - - - - - - maven-compiler-plugin - - - - io.quarkus - quarkus-extension-processor - ${project.version} - - - - - - - - diff --git a/core/devmode/src/main/resources/META-INF/services/io.quarkus.dev.CompilationProvider b/core/devmode/src/main/resources/META-INF/services/io.quarkus.dev.CompilationProvider deleted file mode 100644 index 0fdad837170e6..0000000000000 --- a/core/devmode/src/main/resources/META-INF/services/io.quarkus.dev.CompilationProvider +++ /dev/null @@ -1 +0,0 @@ -io.quarkus.dev.JavaCompilationProvider \ No newline at end of file diff --git a/core/launcher/pom.xml b/core/launcher/pom.xml new file mode 100644 index 0000000000000..c134b926c3718 --- /dev/null +++ b/core/launcher/pom.xml @@ -0,0 +1,59 @@ + + + + quarkus-build-parent + io.quarkus + 999-SNAPSHOT + ../../build-parent/pom.xml + + 4.0.0 + + quarkus-ide-launcher + Quarkus - IDE Launcher + + + + io.quarkus + quarkus-bootstrap-core + + + + + + + maven-shade-plugin + + + + package + + shade + + + true + true + + + *:* + + + + + + + + + + + diff --git a/core/launcher/src/main/java/io/quarkus/launcher/QuarkusLauncher.java b/core/launcher/src/main/java/io/quarkus/launcher/QuarkusLauncher.java new file mode 100644 index 0000000000000..b05742e686b38 --- /dev/null +++ b/core/launcher/src/main/java/io/quarkus/launcher/QuarkusLauncher.java @@ -0,0 +1,50 @@ +package io.quarkus.launcher; + +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.function.Consumer; + +import io.quarkus.bootstrap.app.CuratedApplication; +import io.quarkus.bootstrap.app.QuarkusBootstrap; + +/** + * IDE entry point. + * + * This has a number of hacks to make it work, and is always going to be a bit fragile, as it is hard to make something + * that will work 100% of the time as we just don't have enough information. + * + * The launcher module has all it's dependencies shaded, so it is effectively self contained. This allows deployment time + * code to not leak into runtime code, as the launcher artifact is explicitly excluded from the production build via a + * hard coded exclusion. + * + */ +public class QuarkusLauncher { + + public static void launch(String callingClass, String quarkusApplication, Consumer exitHandler, String... args) { + + String classResource = callingClass.replace(".", "/") + ".class"; + URL resource = Thread.currentThread().getContextClassLoader().getResource(classResource); + String path = resource.getPath(); + path = path.substring(0, path.length() - classResource.length()); + + Path appClasses = Paths.get(path); + if (quarkusApplication != null) { + System.setProperty("quarkus.package.main-class", quarkusApplication); + } + + try { + //todo : proper support for everything + CuratedApplication app = QuarkusBootstrap.builder(appClasses) + .setBaseClassLoader(QuarkusLauncher.class.getClassLoader()) + .setMode(QuarkusBootstrap.Mode.DEV) + .build().bootstrap(); + app.createAugmentor() + .createInitialRuntimeApplication().runMainClass(args); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/core/pom.xml b/core/pom.xml index 3f7685bddce81..04a21b47b35a1 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -17,10 +17,10 @@ deployment runtime processor - devmode builder test-extension devmode-spi + launcher diff --git a/core/runtime/pom.xml b/core/runtime/pom.xml index a959a6da4abdd..7a34e337e8426 100644 --- a/core/runtime/pom.xml +++ b/core/runtime/pom.xml @@ -15,6 +15,12 @@ Quarkus - Core - Runtime + + + io.quarkus + quarkus-bootstrap-core + provided + jakarta.annotation jakarta.annotation-api @@ -27,6 +33,14 @@ jakarta.inject jakarta.inject-api + + io.quarkus + quarkus-ide-launcher + + + io.quarkus + quarkus-development-mode-spi + io.smallrye.config smallrye-config diff --git a/core/runtime/src/main/java/io/quarkus/runtime/Application.java b/core/runtime/src/main/java/io/quarkus/runtime/Application.java index be4f43fd60764..73547954da406 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/Application.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/Application.java @@ -3,44 +3,42 @@ import java.io.Closeable; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.LockSupport; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; -import org.graalvm.nativeimage.ImageInfo; import org.wildfly.common.Assert; import org.wildfly.common.lock.Locks; -import com.oracle.svm.core.OS; - -import io.quarkus.runtime.graal.DiagnosticPrinter; +import io.quarkus.dev.appstate.ApplicationStateNotification; import io.quarkus.runtime.shutdown.ShutdownRecorder; -import sun.misc.Signal; -import sun.misc.SignalHandler; /** * The application base class, which is extended and implemented by a generated class which implements the application - * setup logic. The base class does some basic error checking. + * setup logic. The base class does some basic error checking, and maintains the application state. + * + * Note that this class does not manage the application lifecycle in any way, it is solely responsible for starting and + * stopping the application. + * */ @SuppressWarnings("restriction") public abstract class Application implements Closeable { // WARNING: do not inject a logger here, it's too early: the log manager has not been properly set up yet - private static final String DISABLE_SIGNAL_HANDLERS = "DISABLE_SIGNAL_HANDLERS"; + /** + * The name of the generated application class + */ + public static final String APP_CLASS_NAME = "io.quarkus.runner.ApplicationImpl"; private static final int ST_INITIAL = 0; private static final int ST_STARTING = 1; private static final int ST_STARTED = 2; private static final int ST_STOPPING = 3; private static final int ST_STOPPED = 4; - private static final int ST_EXIT = 5; private final Lock stateLock = Locks.reentrantLock(); private final Condition stateCond = stateLock.newCondition(); - private String name; private int state = ST_INITIAL; - private volatile boolean shutdownRequested; private static volatile Application currentApplication; /** @@ -96,12 +94,14 @@ public final void start(String[] args) { } finally { stateLock.unlock(); } + ApplicationStateNotification.notifyStartupComplete(t); throw t; } stateLock.lock(); try { state = ST_STARTED; stateCond.signalAll(); + ApplicationStateNotification.notifyStartupComplete(null); } finally { stateLock.unlock(); } @@ -157,7 +157,6 @@ public final void stop() { break; } case ST_STOPPED: - case ST_EXIT: return; // all good default: throw Assert.impossibleSwitchCase(state); @@ -175,8 +174,9 @@ public final void stop() { stateLock.lock(); try { state = ST_STOPPED; - Timing.printStopTime(name); + Timing.printStopTime(getName()); stateCond.signalAll(); + ApplicationStateNotification.notifyApplicationStopped(); } finally { stateLock.unlock(); } @@ -189,68 +189,7 @@ public static Application currentApplication() { protected abstract void doStop(); - public void setName(String name) { - this.name = name; - } - - /** - * Run the application as if it were in a standalone JVM. - */ - public final void run(String[] args) { - try { - if (ImageInfo.inImageRuntimeCode() && System.getenv(DISABLE_SIGNAL_HANDLERS) == null) { - final SignalHandler handler = new SignalHandler() { - @Override - public void handle(Signal signal) { - System.exit(signal.getNumber() + 0x80); - } - }; - final SignalHandler quitHandler = new SignalHandler() { - @Override - public void handle(Signal signal) { - DiagnosticPrinter.printDiagnostics(System.out); - } - }; - handleSignal("INT", handler); - handleSignal("TERM", handler); - // the HUP and QUIT signals are not defined for the Windows OpenJDK implementation: - // https://hg.openjdk.java.net/jdk8u/jdk8u-dev/hotspot/file/7d5c800dae75/src/os/windows/vm/jvm_windows.cpp - if (OS.getCurrent() == OS.WINDOWS) { - handleSignal("BREAK", quitHandler); - } else { - handleSignal("HUP", handler); - handleSignal("QUIT", quitHandler); - } - } - - final ShutdownHookThread shutdownHookThread = new ShutdownHookThread(Thread.currentThread()); - Runtime.getRuntime().addShutdownHook(shutdownHookThread); - start(args); - try { - while (!shutdownRequested) { - Thread.interrupted(); - LockSupport.park(shutdownHookThread); - } - } finally { - stop(); - } - } finally { - exit(); - } - } - - private void exit() { - stateLock.lock(); - try { - System.out.flush(); - System.err.flush(); - state = ST_EXIT; - stateCond.signalAll(); - // code beyond this point may not run - } finally { - stateLock.unlock(); - } - } + public abstract String getName(); private static IllegalStateException interruptedOnAwaitStart() { return new IllegalStateException("Interrupted while waiting for another thread to start the application"); @@ -260,42 +199,29 @@ private static IllegalStateException interruptedOnAwaitStop() { return new IllegalStateException("Interrupted while waiting for another thread to stop the application"); } - private static void handleSignal(final String signal, final SignalHandler handler) { + public void awaitShutdown() { + final Lock stateLock = this.stateLock; + stateLock.lock(); try { - Signal.handle(new Signal(signal), handler); - } catch (IllegalArgumentException ignored) { - // Do nothing - } - } - - class ShutdownHookThread extends Thread { - private final Thread mainThread; - - ShutdownHookThread(Thread mainThread) { - super("Shutdown thread"); - this.mainThread = mainThread; - setDaemon(false); - } - - @Override - public void run() { - shutdownRequested = true; - LockSupport.unpark(mainThread); - final Lock stateLock = Application.this.stateLock; - final Condition stateCond = Application.this.stateCond; - stateLock.lock(); - try { - while (state != ST_EXIT) { - stateCond.awaitUninterruptibly(); + for (;;) { + switch (state) { + case ST_STOPPED: + return; // all good + default: + try { + stateCond.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw interruptedOnAwaitStop(); + } } - } finally { - stateLock.unlock(); } + } finally { + stateLock.unlock(); } + } - @Override - public String toString() { - return getName(); - } + public boolean isStarted() { + return state == ST_STARTED; } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java new file mode 100644 index 0000000000000..bedaa4455f2a7 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java @@ -0,0 +1,308 @@ +package io.quarkus.runtime; + +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.function.Consumer; + +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.Any; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.CDI; + +import org.graalvm.nativeimage.ImageInfo; +import org.jboss.logging.Logger; +import org.wildfly.common.lock.Locks; + +import com.oracle.svm.core.OS; + +import io.quarkus.runtime.graal.DiagnosticPrinter; +import sun.misc.Signal; +import sun.misc.SignalHandler; + +/** + * Manages the lifecycle of a Quarkus application. + * + * The {@link Application} class is responsible for starting and stopping the application, + * but nothing else. This class can be used to run both persistent application 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 handled + * exiting with the supplied exit code. + * + * 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. + * + * This class is static, there can only every be a single application instance running at any time. + * + */ +public class ApplicationLifecycleManager { + + private static volatile Consumer defaultExitCodeHandler = new Consumer() { + @Override + public void accept(Integer integer) { + System.exit(integer); + } + }; + + private ApplicationLifecycleManager() { + + } + // WARNING: do not inject a logger here, it's too early: the log manager has not been properly set up yet + + private static final String DISABLE_SIGNAL_HANDLERS = "DISABLE_SIGNAL_HANDLERS"; + + //guard for all state + private static final Lock stateLock = Locks.reentrantLock(); + private static final Condition stateCond = stateLock.newCondition(); + + private static int exitCode = -1; + private static boolean shutdownRequested; + private static Application currentApplication; + private static boolean hooksRegistered; + private static boolean vmShuttingDown; + + public static final void run(Application application, String... args) { + run(application, null, null, args); + } + + public static final void run(Application application, Class quarkusApplication, + Consumer 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 + boolean alreadyStarted = application.isStarted(); + if (!hooksRegistered) { + registerHooks(); + hooksRegistered = true; + } + if (currentApplication != null && !shutdownRequested) { + throw new IllegalStateException("Quarkus already running"); + } + try { + exitCode = -1; + shutdownRequested = false; + currentApplication = application; + } finally { + stateLock.unlock(); + } + application.start(args); + //now we are started, we either run the main application or just wait to exit + try { + if (quarkusApplication != null) { + Set> beans = CDI.current().getBeanManager().getBeans(quarkusApplication, new Any.Literal()); + Bean bean = null; + for (Bean i : beans) { + if (i.getBeanClass() == quarkusApplication) { + bean = i; + break; + } + } + QuarkusApplication instance; + if (bean == null) { + instance = quarkusApplication.newInstance(); + } else { + CreationalContext ctx = CDI.current().getBeanManager().createCreationalContext(bean); + instance = (QuarkusApplication) CDI.current().getBeanManager().getReference(bean, + quarkusApplication, ctx); + } + int result = -1; + try { + result = instance.run(args);//TODO: argument filtering? + } finally { + stateLock.lock(); + try { + //now we exit + if (exitCode == -1 && result != -1) { + exitCode = result; + } + shutdownRequested = true; + stateCond.signalAll(); + } finally { + stateLock.unlock(); + } + } + } else { + stateLock.lock(); + try { + while (!shutdownRequested) { + Thread.interrupted(); + stateCond.await(); + } + } finally { + stateLock.unlock(); + } + } + } catch (Exception e) { + Logger.getLogger(Application.class).error("Error running Quarkus application", e); + stateLock.lock(); + try { + shutdownRequested = true; + stateCond.signalAll(); + } finally { + stateLock.unlock(); + } + application.stop(); + (exitCodeHandler == null ? defaultExitCodeHandler : exitCodeHandler).accept(1); + 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 + } + + private static void registerHooks() { + if (ImageInfo.inImageRuntimeCode() && System.getenv(DISABLE_SIGNAL_HANDLERS) == null) { + registerSignalHandlers(); + } + final ShutdownHookThread shutdownHookThread = new ShutdownHookThread(); + Runtime.getRuntime().addShutdownHook(shutdownHookThread); + } + + private static void registerSignalHandlers() { + final SignalHandler handler = new SignalHandler() { + @Override + public void handle(Signal signal) { + System.exit(signal.getNumber() + 0x80); + } + }; + final SignalHandler quitHandler = new SignalHandler() { + @Override + public void handle(Signal signal) { + DiagnosticPrinter.printDiagnostics(System.out); + } + }; + handleSignal("INT", handler); + handleSignal("TERM", handler); + // the HUP and QUIT signals are not defined for the Windows OpenJDK implementation: + // https://hg.openjdk.java.net/jdk8u/jdk8u-dev/hotspot/file/7d5c800dae75/src/os/windows/vm/jvm_windows.cpp + if (OS.getCurrent() == OS.WINDOWS) { + handleSignal("BREAK", quitHandler); + } else { + handleSignal("HUP", handler); + handleSignal("QUIT", quitHandler); + } + } + + /** + * + * @return The current exit code that would be reported if the application exits + */ + public static int getExitCode() { + return exitCode == -1 ? 0 : exitCode; + } + + /** + * Exits without supplying an exit code. + * + * The application will exit with a code of 0 by default, however if this method is called it is still possible + * for a different exit code to be set. + */ + public static void exit() { + exit(-1); + } + + public static Consumer getDefaultExitCodeHandler() { + return defaultExitCodeHandler; + } + + /** + * + * @return true if the VM is shutting down + */ + public static boolean isVmShuttingDown() { + return vmShuttingDown; + } + + /** + * 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 + */ + public static void setDefaultExitCodeHandler(Consumer defaultExitCodeHandler) { + Objects.requireNonNull(defaultExitCodeHandler); + ApplicationLifecycleManager.defaultExitCodeHandler = defaultExitCodeHandler; + } + + /** + * Signals that the application should exit with the given code. + * + * Note that the first positive exit code will 'win', so if the exit code + * has already been set then the exit code will be ignored. + * + * @param code The exit code + */ + public static void exit(int code) { + stateLock.lock(); + try { + if (code >= 0 && exitCode == -1) { + exitCode = code; + } + if (shutdownRequested) { + return; + } + shutdownRequested = true; + stateCond.signalAll(); + } finally { + stateLock.unlock(); + } + } + + /** + * Waits for the shutdown process to be initiated. + */ + public static void waitForExit() { + stateLock.lock(); + try { + while (!shutdownRequested) { + stateCond.awaitUninterruptibly(); + } + } finally { + stateLock.unlock(); + } + } + + static class ShutdownHookThread extends Thread { + + ShutdownHookThread() { + super("Shutdown thread"); + setDaemon(false); + } + + @Override + public void run() { + stateLock.lock(); + vmShuttingDown = true; + //we just request shutdown and unblock the main thread + //we let the application main thread take care of actually exiting + //TODO: if the main thread is not actively waiting to exit should we interrupt it? + shutdownRequested = true; + try { + stateCond.signalAll(); + } finally { + stateLock.unlock(); + } + currentApplication.awaitShutdown(); + System.out.flush(); + System.err.flush(); + } + + @Override + public String toString() { + return getName(); + } + } + + private static void handleSignal(final String signal, final SignalHandler handler) { + try { + Signal.handle(new Signal(signal), handler); + } catch (IllegalArgumentException ignored) { + // Do nothing + } + } +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/Quarkus.java b/core/runtime/src/main/java/io/quarkus/runtime/Quarkus.java new file mode 100644 index 0000000000000..33b562870eb4d --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/Quarkus.java @@ -0,0 +1,168 @@ +package io.quarkus.runtime; + +import java.util.function.Consumer; + +import org.jboss.logging.Logger; + +import io.quarkus.launcher.QuarkusLauncher; + +/** + * The entry point for applications that use a main method. Quarkus will shut down when the main method returns. + * + * If this application has already been generated then it will be run directly, otherwise it will be launched + * in dev mode and augmentation will be done automatically. + * + * If an application does not want to immediatly shut down then {@link #waitForExit()} should be called, which + * will block until shutdown is initiated, either from an external signal or by a call to one of the exit methods. + * + * If no main class is specified then one is generated automatically that will simply wait to exit after Quarkus is booted. + * + */ +public class Quarkus { + + //WARNING: this is too early to inject a logger + //private static final Logger log = Logger.getLogger(Quarkus.class); + + /** + * Runs a quarkus application, that will run until the provided {@link QuarkusApplication} has completed. + * + * Note that if this is run from the IDE the application will run in a different class loader to the + * calling class. It is recommended that the calling class do no logic, and instead this logic should + * go into the QuarkusApplication. + * + * @param quarkusApplication The application to run, or null + * @param args The command line parameters + */ + public static void run(Class quarkusApplication, String... args) { + run(quarkusApplication, null, args); + } + + /** + * Runs a quarkus application, that will run until the provided {@link QuarkusApplication} has completed. + * + * Note that if this is run from the IDE the application will run in a different class loader to the + * calling class. It is recommended that the calling class do no logic, and instead this logic should + * 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 args The command line parameters + */ + public static void run(Class quarkusApplication, Consumer exitHandler, + String... args) { + try { + //production and common dev mode path + //we already have an application, run it directly + Class appClass = (Class) Class.forName(Application.APP_CLASS_NAME, + false, Thread.currentThread().getContextClassLoader()); + Application application = appClass.newInstance(); + ApplicationLifecycleManager.run(application, quarkusApplication, exitHandler, args); + return; + } 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); + } else { + ApplicationLifecycleManager.getDefaultExitCodeHandler().accept(1); + } + 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); + + } + + private static void launchFromIDE(Class quarkusApplication, Consumer exitHandler, + 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(); + int pos = 2; + while (stackTrace[pos].getClassName().equals(Quarkus.class.getName())) { + pos++; + } + String callingClass = stackTrace[pos].getClassName(); + QuarkusLauncher.launch(callingClass, quarkusApplication == null ? null : quarkusApplication.getName(), exitHandler, + args); + } + + /** + * Starts a quarkus application, that will run until it either receives a signal (e.g. user presses ctrl+c) + * or one of the exit methods is called. + * + * This method does not return, as System.exit() is called after the application is finished. + * + * @param args The command line parameters + */ + public static void run(String... args) { + run(null, args); + } + + /** + * Exits the application in an async manner. Calling this method + * will initiate the Quarkus shutdown process, and then immediately return. + * + * This method will unblock the {@link #waitForExit()} method. + * + * Note that if the main thread is executing a Quarkus application this will only take + * effect if {@link #waitForExit()} has been called, otherwise the application will continue + * to execute (i.e. this does not initiate the shutdown process, it just signals the main + * thread that the application is done so that shutdown can run when the main thread returns). + * + * The error code supplied here will override the value returned from the main application. + * + * @param code The exit code. This may be overridden if an exception occurs on shutdown + */ + public static void asyncExit(int code) { + ApplicationLifecycleManager.exit(code); + } + + /** + * Exits the application in an async manner. Calling this method + * will initiate the Quarkus shutdown process, and then immediately return. + * + * This method will unblock the {@link #waitForExit()} method. + * + * Note that if the main thread is executing a Quarkus application this will only take + * effect if {@link #waitForExit()} has been called, otherwise the application will continue + * to execute (i.e. this does not initiate the shutdown process, it just signals the main + * thread that the application is done so that shutdown can run when the main thread returns). + * + */ + public static void asyncExit() { + ApplicationLifecycleManager.exit(-1); + } + + /** + * Method that will block until the Quarkus shutdown process is initiated. + * + * Note that this unblocks as soon as the shutdown process starts, not after it + * has finished. + * + * {@link QuarkusApplication} implementations that wish to run some logic on startup, and + * then run should call this method. + */ + public static void waitForExit() { + ApplicationLifecycleManager.waitForExit(); + + } + + /** + * Starts the shutdown process, then waits for the application to shut down. + * + * Must not be called by the main thread, or a deadlock will result. + */ + public static void blockingExit() { + Application app = Application.currentApplication(); + asyncExit(); + if (app != null) { + app.awaitShutdown(); + } + } +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/QuarkusApplication.java b/core/runtime/src/main/java/io/quarkus/runtime/QuarkusApplication.java new file mode 100644 index 0000000000000..350685123d513 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/QuarkusApplication.java @@ -0,0 +1,6 @@ +package io.quarkus.runtime; + +public interface QuarkusApplication { + + int run(String... args) throws Exception; +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/StartupContext.java b/core/runtime/src/main/java/io/quarkus/runtime/StartupContext.java index be10ea5c5e492..bae846efad0ae 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/StartupContext.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/StartupContext.java @@ -6,11 +6,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import org.jboss.logging.Logger; public class StartupContext implements Closeable { + public static final String RAW_COMMAND_LINE_ARGS = StartupContext.class.getName() + ".raw-command-line-args"; + private static final Logger LOG = Logger.getLogger(StartupContext.class); private final Map values = new HashMap<>(); @@ -30,9 +33,19 @@ public void addLastShutdownTask(Runnable runnable) { lastShutdownTasks.add(runnable); } }; + private String[] commandLineArgs; public StartupContext() { values.put(ShutdownContext.class.getName(), shutdownContext); + values.put(RAW_COMMAND_LINE_ARGS, new Supplier() { + @Override + public String[] get() { + if (commandLineArgs == null) { + throw new RuntimeException("Command line arguments not available during static init"); + } + return commandLineArgs; + } + }); } public void putValue(String name, Object value) { @@ -72,4 +85,9 @@ private void runAllInReverseOrder(List tasks) { } } } + + @SuppressWarnings("unused") + public void setCommandLineArguments(String[] commandLineArguments) { + this.commandLineArgs = commandLineArguments; + } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/annotations/CommandLineArguments.java b/core/runtime/src/main/java/io/quarkus/runtime/annotations/CommandLineArguments.java new file mode 100644 index 0000000000000..0db283ba1ba1a --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/annotations/CommandLineArguments.java @@ -0,0 +1,15 @@ +package io.quarkus.runtime.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** + * A qualifier that can be used to inject the command line arguments. + */ +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface CommandLineArguments { + +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/annotations/DefaultMain.java b/core/runtime/src/main/java/io/quarkus/runtime/annotations/DefaultMain.java new file mode 100644 index 0000000000000..3ec1a41a41241 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/annotations/DefaultMain.java @@ -0,0 +1,29 @@ +package io.quarkus.runtime.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The default main class of a Quarkus application. This annotation can appear + * at most once in a Quarkus application. + * + * There are two different ways this annotation can be used. The first is to place it + * on a class with a Java main method. This main method will be the default entry point of + * the application. Note that Quarkus will not be started when this is method is called, + * this method must launch Quarkus with the {@link io.quarkus.runtime.Quarkus#run(Class, String...)} + * method. + * + * Alternatively this annotation can be placed on an {@link io.quarkus.runtime.QuarkusApplication} + * implementation. In this case a main method is automatically generated that will automatically + * call {@link io.quarkus.runtime.Quarkus#run(Class, String...)} with the provided application. + * + * Note that this can be overridden by the presence of the {@literal quarkus.package.main-class} + * configuration key, in which case the annotation is ignored. + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DefaultMain { +} diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/commandmode/ExitCodeTestCase.java b/core/test-extension/deployment/src/test/java/io/quarkus/commandmode/ExitCodeTestCase.java new file mode 100644 index 0000000000000..3e223671654b4 --- /dev/null +++ b/core/test-extension/deployment/src/test/java/io/quarkus/commandmode/ExitCodeTestCase.java @@ -0,0 +1,96 @@ +package io.quarkus.commandmode; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.Application; +import io.quarkus.runtime.ApplicationLifecycleManager; +import io.quarkus.runtime.Quarkus; +import io.quarkus.runtime.QuarkusApplication; +import io.quarkus.test.QuarkusUnitTest; + +public class ExitCodeTestCase { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Test + public void testReturnedExitCode() throws ExecutionException, InterruptedException { + CompletableFuture future = new CompletableFuture<>(); + ApplicationLifecycleManager.run(Application.currentApplication(), ExitCodeApplication.class, new Consumer() { + @Override + public void accept(Integer integer) { + future.complete(integer); + } + }, "5"); + Assertions.assertEquals(5, future.get()); + } + + @Test + public void testWaitToExitWithCode() throws ExecutionException, InterruptedException { + CompletableFuture future = new CompletableFuture<>(); + new Thread(new Runnable() { + @Override + public void run() { + ApplicationLifecycleManager.run(Application.currentApplication(), WaitToExitApplication.class, + new Consumer() { + @Override + public void accept(Integer integer) { + future.complete(integer); + } + }); + } + }).start(); + Thread.sleep(500); + Assertions.assertFalse(future.isDone()); + Quarkus.asyncExit(10); + Assertions.assertEquals(10, future.get()); + } + + @Test + public void testWaitToExitWithNoCode() throws ExecutionException, InterruptedException { + CompletableFuture future = new CompletableFuture<>(); + new Thread(new Runnable() { + @Override + public void run() { + ApplicationLifecycleManager.run(Application.currentApplication(), WaitToExitApplication.class, + new Consumer() { + @Override + public void accept(Integer integer) { + future.complete(integer); + } + }); + } + }).start(); + Thread.sleep(500); + Assertions.assertFalse(future.isDone()); + Quarkus.asyncExit(); + Assertions.assertEquals(1, future.get()); + } + + public static class ExitCodeApplication implements QuarkusApplication { + + @Override + public int run(String... args) throws Exception { + return Integer.parseInt(args[0]); + } + } + + public static class WaitToExitApplication implements QuarkusApplication { + + @Override + public int run(String... args) throws Exception { + Quarkus.waitForExit(); + return 1; + } + } + +} diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/commandmode/HelloWorldMain.java b/core/test-extension/deployment/src/test/java/io/quarkus/commandmode/HelloWorldMain.java new file mode 100644 index 0000000000000..1c34788b55fcd --- /dev/null +++ b/core/test-extension/deployment/src/test/java/io/quarkus/commandmode/HelloWorldMain.java @@ -0,0 +1,13 @@ +package io.quarkus.commandmode; + +import io.quarkus.runtime.QuarkusApplication; +import io.quarkus.runtime.annotations.DefaultMain; + +@DefaultMain +public class HelloWorldMain implements QuarkusApplication { + @Override + public int run(String... args) throws Exception { + System.out.println("Hello World"); + return 10; + } +} diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/commandmode/HelloWorldNonDefault.java b/core/test-extension/deployment/src/test/java/io/quarkus/commandmode/HelloWorldNonDefault.java new file mode 100644 index 0000000000000..9753b91389c27 --- /dev/null +++ b/core/test-extension/deployment/src/test/java/io/quarkus/commandmode/HelloWorldNonDefault.java @@ -0,0 +1,11 @@ +package io.quarkus.commandmode; + +import io.quarkus.runtime.QuarkusApplication; + +public class HelloWorldNonDefault implements QuarkusApplication { + @Override + public int run(String... args) throws Exception { + System.out.println("Hello World"); + return 10; + } +} diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/commandmode/JavaMain.java b/core/test-extension/deployment/src/test/java/io/quarkus/commandmode/JavaMain.java new file mode 100644 index 0000000000000..ce78963294376 --- /dev/null +++ b/core/test-extension/deployment/src/test/java/io/quarkus/commandmode/JavaMain.java @@ -0,0 +1,12 @@ +package io.quarkus.commandmode; + +import io.quarkus.runtime.Quarkus; +import io.quarkus.runtime.annotations.DefaultMain; + +@DefaultMain +public class JavaMain { + + public static void main(String... args) { + Quarkus.run(HelloWorldNonDefault.class, args); + } +} diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/commandmode/JavaMainTestCase.java b/core/test-extension/deployment/src/test/java/io/quarkus/commandmode/JavaMainTestCase.java new file mode 100644 index 0000000000000..8b78be44b307c --- /dev/null +++ b/core/test-extension/deployment/src/test/java/io/quarkus/commandmode/JavaMainTestCase.java @@ -0,0 +1,28 @@ +package io.quarkus.commandmode; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusProdModeTest; + +public class JavaMainTestCase { + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsManifestResource("application.properties", "microprofile-config.properties") + .addClasses(JavaMain.class, HelloWorldNonDefault.class)) + .setApplicationName("run-exit") + .setApplicationVersion("0.1-SNAPSHOT") + .setExpectExit(true) + .setRun(true); + + @Test + public void testRun() { + Assertions.assertTrue(config.getStartupConsoleOutput().contains("Hello World")); + Assertions.assertEquals(10, config.getExitCode()); + } + +} diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/commandmode/SimpleCommandModeTestCase.java b/core/test-extension/deployment/src/test/java/io/quarkus/commandmode/SimpleCommandModeTestCase.java new file mode 100644 index 0000000000000..e4ce37c23980c --- /dev/null +++ b/core/test-extension/deployment/src/test/java/io/quarkus/commandmode/SimpleCommandModeTestCase.java @@ -0,0 +1,28 @@ +package io.quarkus.commandmode; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusProdModeTest; + +public class SimpleCommandModeTestCase { + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsManifestResource("application.properties", "microprofile-config.properties") + .addClasses(HelloWorldMain.class)) + .setApplicationName("run-exit") + .setApplicationVersion("0.1-SNAPSHOT") + .setExpectExit(true) + .setRun(true); + + @Test + public void testRun() { + Assertions.assertTrue(config.getStartupConsoleOutput().contains("Hello World")); + Assertions.assertEquals(10, config.getExitCode()); + } + +} diff --git a/devtools/gradle/build.gradle b/devtools/gradle/build.gradle index 35f0a39e0a4ed..6c967debc75f1 100644 --- a/devtools/gradle/build.gradle +++ b/devtools/gradle/build.gradle @@ -24,7 +24,7 @@ dependencies { implementation "io.quarkus:quarkus-devtools-common:${version}" implementation "io.quarkus:quarkus-platform-descriptor-json:${version}" implementation "io.quarkus:quarkus-platform-descriptor-resolver-json:${version}" - implementation "io.quarkus:quarkus-development-mode:${version}" + implementation "io.quarkus:quarkus-core-deployment:${version}" testImplementation 'org.mockito:mockito-core:3.2.4' testImplementation 'org.assertj:assertj-core:3.14.0' diff --git a/devtools/gradle/pom.xml b/devtools/gradle/pom.xml index 627d81e9563c2..36f9c00743a1f 100644 --- a/devtools/gradle/pom.xml +++ b/devtools/gradle/pom.xml @@ -29,11 +29,11 @@ io.quarkus - quarkus-devtools-common + quarkus-core-deployment io.quarkus - quarkus-development-mode + quarkus-devtools-common io.quarkus diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java index cf719f67bfd92..692394b0e88fa 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java @@ -59,8 +59,8 @@ import io.quarkus.bootstrap.model.AppModel; import io.quarkus.bootstrap.resolver.AppModelResolver; import io.quarkus.bootstrap.resolver.AppModelResolverException; -import io.quarkus.dev.DevModeContext; -import io.quarkus.dev.DevModeMain; +import io.quarkus.deployment.dev.DevModeContext; +import io.quarkus.deployment.dev.DevModeMain; import io.quarkus.gradle.QuarkusPluginExtension; import io.quarkus.utilities.JavaBinFinder; diff --git a/devtools/maven/pom.xml b/devtools/maven/pom.xml index 1082f1a9e6138..3a31ca8834a54 100644 --- a/devtools/maven/pom.xml +++ b/devtools/maven/pom.xml @@ -22,6 +22,10 @@ io.quarkus quarkus-bootstrap-core + + io.quarkus + quarkus-core-deployment + io.quarkus quarkus-platform-descriptor-json @@ -71,10 +75,6 @@ maven-plugin-annotations provided - - io.quarkus - quarkus-development-mode - org.apache.maven.shared maven-artifact-transfer diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java index c70fecf01233e..0d8948d0ccca2 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java @@ -61,8 +61,8 @@ import org.twdata.maven.mojoexecutor.MojoExecutor; import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject; -import io.quarkus.dev.DevModeContext; -import io.quarkus.dev.DevModeMain; +import io.quarkus.deployment.dev.DevModeContext; +import io.quarkus.deployment.dev.DevModeMain; import io.quarkus.maven.components.MavenVersionEnforcer; import io.quarkus.maven.utilities.MojoUtils; import io.quarkus.utilities.JavaBinFinder; @@ -581,18 +581,21 @@ void prepare() throws Exception { pomFiles.add(project.getDir().resolve("pom.xml")); } } - - final DefaultArtifact devModeJar = new DefaultArtifact("io.quarkus", "quarkus-development-mode", "jar", + DefaultArtifact bootstrap = new DefaultArtifact("io.quarkus", "quarkus-core-deployment", "jar", pluginDef.getVersion()); final DependencyResult cpRes = repoSystem.resolveDependencies(repoSession, new DependencyRequest() .setCollectRequest( new CollectRequest() - .setRoot(new org.eclipse.aether.graph.Dependency(devModeJar, JavaScopes.RUNTIME)) + .setRoot(new org.eclipse.aether.graph.Dependency(bootstrap, JavaScopes.RUNTIME)) .setRepositories(repos))); for (ArtifactResult appDep : cpRes.getArtifactResults()) { - addToClassPaths(classPathManifest, devModeContext, appDep.getArtifact().getFile()); + //we only use the launcher for launching from the IDE, we need to exclude it + if (!(appDep.getArtifact().getGroupId().equals("io.quarkus") + && appDep.getArtifact().getArtifactId().equals("quarkus-ide-launcher"))) { + addToClassPaths(classPathManifest, devModeContext, appDep.getArtifact().getFile()); + } } args.add("-Djava.util.logging.manager=org.jboss.logmanager.LogManager"); diff --git a/docs/src/main/asciidoc/command-mode-reference.adoc b/docs/src/main/asciidoc/command-mode-reference.adoc new file mode 100644 index 0000000000000..ec13ed70b0871 --- /dev/null +++ b/docs/src/main/asciidoc/command-mode-reference.adoc @@ -0,0 +1,84 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc +//// += Quarkus - Command Mode Applications + +include::./attributes.adoc[] + +This reference covers how to write application that run and then exit. + +== Writing Command Mode Applications + +There are two different approaches that can be used to implement applications +that exit. + +. Implement `QuarkusApplication` and have Quarkus run this method automatically +. Implement `QuarkusApplication` and a Java main method, and use the Java main method to launch Quarkus + +In this document the `QuarkusApplication` instance is referred to as the application main, +and a class with a Java main method is the Java main. + +The simplest possible command mode application might appear as follows: + +[source,java] +---- +import io.quarkus.runtime.QuarkusApplication; +import io.quarkus.runtime.annotations.DefaultMain; + +@DefaultMain // <1> +public class HelloWorldMain implements QuarkusApplication { + @Override + public int run(String... args) throws Exception { // <2> + System.out.println("Hello World"); + return 10; + } +} +---- +<1> The `@DefaultMain` annotation to tells Quarkus that this is the main entry point. There can only be a single instance of this annotation in the application. +<2> The `run` method is invoked once Quarkus starts, and the application stops when it finishes. + +If we want to use a Java main to run the application main it would look like: + +[source,java] +---- +import io.quarkus.runtime.Quarkus; +import io.quarkus.runtime.annotations.DefaultMain; + +@DefaultMain +public class JavaMain { + + public static void main(String... args) { + Quarkus.run(HelloWorldMain.class, args); + } +} +---- + +This is effectively the same as running the `HelloWorldMain` application main directly, but has the advantage it can +be run from the IDE. + +WARNING: It is recommended that a Java main perform very little logic, and just +launch the application main. In development mode the Java main will run in a +different ClassLoader to the main application, so may not behave as you would +expect. + +It is possible to specify/override the main by using the `quarkus.package.main-class` build time +configuration option. + + + +=== The command mode lifecycle + +When running a command mode application the basic lifecycle is as follows: + +. Start Quarkus +. Run the `QuarkusApplication` main method +. Shutdown Quarkus and exit the JVM after the main method returns + +Shutdown is always initiated by the application main thread returning. If you want to run some logic on startup, +and then run like a normal application (i.e. not exit) then you should call `Quarkus.waitForExit` from the main +thread (A non-command mode application is essentially just running an application that just calls `waitForExit`). + +If you want to shut down a running application and you are not in the main thread then you should call `Quarkus.asyncExit` +in order to unblock the main thread and initiate the shutdown process. diff --git a/docs/src/main/asciidoc/index.adoc b/docs/src/main/asciidoc/index.adoc index 4569ece9570f4..b9fc6870a34f8 100644 --- a/docs/src/main/asciidoc/index.adoc +++ b/docs/src/main/asciidoc/index.adoc @@ -59,6 +59,7 @@ include::quarkus-intro.adoc[tag=intro] * link:jms.html[Using Artemis JMS Client] * link:reactive-routes.adoc[Using Reactive Routes] * link:camel.adoc[Apache Camel] +* link:command-mode-reference.adoc[Command Mode Applications] * link:faq.html[FAQs] diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java index 1a0c2bb78f706..d8b9065f599a4 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java @@ -15,6 +15,8 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +import javax.enterprise.context.ApplicationScoped; + import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; @@ -56,6 +58,7 @@ import io.quarkus.deployment.builditem.ApplicationClassPredicateBuildItem; import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem; import io.quarkus.deployment.builditem.CapabilityBuildItem; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.ExecutorBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; @@ -65,6 +68,7 @@ import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveFieldBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem; +import io.quarkus.runtime.QuarkusApplication; /** * This class contains build steps that trigger various phases of the bean processing. @@ -91,6 +95,16 @@ CapabilityBuildItem capability() { return new CapabilityBuildItem(Capabilities.CDI_ARC); } + @BuildStep + AdditionalBeanBuildItem quarkusApplication(CombinedIndexBuildItem combinedIndexBuildItem) { + return AdditionalBeanBuildItem.builder().setUnremovable() + .setDefaultScope(DotName.createSimple(ApplicationScoped.class.getName())) + .addBeanClasses(combinedIndexBuildItem.getIndex() + .getAllKnownImplementors(DotName.createSimple(QuarkusApplication.class.getName())).stream() + .map(s -> s.name().toString()).toArray(String[]::new)) + .build(); + } + // PHASE 1 - build BeanProcessor, register custom contexts @BuildStep public ContextRegistrationPhaseBuildItem initialize( diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/CommandLineArgumentsProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/CommandLineArgumentsProcessor.java new file mode 100644 index 0000000000000..25369b165810a --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/CommandLineArgumentsProcessor.java @@ -0,0 +1,26 @@ +package io.quarkus.arc.deployment; + +import io.quarkus.arc.runtime.ArcRecorder; +import io.quarkus.arc.runtime.CommandLineArgumentsProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.RawCommandLineArgumentsBuildItem; +import io.quarkus.runtime.annotations.CommandLineArguments; + +public class CommandLineArgumentsProcessor { + + @BuildStep + @Record(ExecutionTime.STATIC_INIT) + BeanContainerListenerBuildItem commandLineArgs(RawCommandLineArgumentsBuildItem rawCommandLineArgumentsBuildItem, + ArcRecorder arcRecorder) { + //todo: this should be filtered + return new BeanContainerListenerBuildItem(arcRecorder.initCommandLineArgs(rawCommandLineArgumentsBuildItem)); + } + + @BuildStep + AdditionalBeanBuildItem qualifier() { + return AdditionalBeanBuildItem.builder().setUnremovable() + .addBeanClasses(CommandLineArguments.class, CommandLineArgumentsProducer.class).build(); + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/arguments/CommandLineArgumentsTestCase.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/arguments/CommandLineArgumentsTestCase.java new file mode 100644 index 0000000000000..3b330988106c4 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/arguments/CommandLineArgumentsTestCase.java @@ -0,0 +1,30 @@ +package io.quarkus.arc.test.arguments; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.annotations.CommandLineArguments; +import io.quarkus.test.QuarkusUnitTest; + +public class CommandLineArgumentsTestCase { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setCommandLineParameters("Hello", "World") + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Inject + @CommandLineArguments + String[] args; + + @Test + public void testConfigWasInjected() { + Assertions.assertArrayEquals(new String[] { "Hello", "World" }, args); + } + +} diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java index 0d4ac9109fa37..1d3e3125d2067 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java @@ -124,6 +124,15 @@ public Object get() { }; } + public BeanContainerListener initCommandLineArgs(Supplier args) { + return new BeanContainerListener() { + @Override + public void created(BeanContainer container) { + container.instance(CommandLineArgumentsProducer.class).setCommandLineArgs(args); + } + }; + } + private static final class DefaultInstanceFactory implements BeanContainer.Factory { final Class type; diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/CommandLineArgumentsProducer.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/CommandLineArgumentsProducer.java new file mode 100644 index 0000000000000..026285d4e8eac --- /dev/null +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/CommandLineArgumentsProducer.java @@ -0,0 +1,24 @@ +package io.quarkus.arc.runtime; + +import java.util.function.Supplier; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; + +import io.quarkus.runtime.annotations.CommandLineArguments; + +@ApplicationScoped +public class CommandLineArgumentsProducer { + + private volatile Supplier commandLineArgs; + + @Produces + @CommandLineArguments + public String[] getCommandLineArgs() { + return commandLineArgs.get(); + } + + public void setCommandLineArgs(Supplier commandLineArgs) { + this.commandLineArgs = commandLineArgs; + } +} diff --git a/extensions/kogito/deployment/pom.xml b/extensions/kogito/deployment/pom.xml index c3459f8b607a5..8e9b647a86dc4 100644 --- a/extensions/kogito/deployment/pom.xml +++ b/extensions/kogito/deployment/pom.xml @@ -14,10 +14,6 @@ - - io.quarkus - quarkus-development-mode - io.quarkus quarkus-core-deployment diff --git a/extensions/kogito/deployment/src/main/java/io/quarkus/kogito/deployment/KogitoCompilationProvider.java b/extensions/kogito/deployment/src/main/java/io/quarkus/kogito/deployment/KogitoCompilationProvider.java index bec9fe278bd53..aa7c781fa34ae 100644 --- a/extensions/kogito/deployment/src/main/java/io/quarkus/kogito/deployment/KogitoCompilationProvider.java +++ b/extensions/kogito/deployment/src/main/java/io/quarkus/kogito/deployment/KogitoCompilationProvider.java @@ -19,7 +19,7 @@ import org.kie.kogito.codegen.context.QuarkusKogitoBuildContext; import org.kie.kogito.codegen.di.CDIDependencyInjectionAnnotator; -import io.quarkus.dev.JavaCompilationProvider; +import io.quarkus.deployment.dev.JavaCompilationProvider; public abstract class KogitoCompilationProvider extends JavaCompilationProvider { diff --git a/extensions/kogito/deployment/src/main/resources/META-INF/services/io.quarkus.dev.CompilationProvider b/extensions/kogito/deployment/src/main/resources/META-INF/services/io.quarkus.deployment.dev.CompilationProvider similarity index 100% rename from extensions/kogito/deployment/src/main/resources/META-INF/services/io.quarkus.dev.CompilationProvider rename to extensions/kogito/deployment/src/main/resources/META-INF/services/io.quarkus.deployment.dev.CompilationProvider diff --git a/extensions/kotlin/deployment/pom.xml b/extensions/kotlin/deployment/pom.xml index 67622c54c0030..9362bf3b77e83 100644 --- a/extensions/kotlin/deployment/pom.xml +++ b/extensions/kotlin/deployment/pom.xml @@ -24,11 +24,6 @@ quarkus-jackson-spi - - io.quarkus - quarkus-development-mode - - org.jetbrains.kotlin kotlin-compiler diff --git a/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinCompilationProvider.java b/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinCompilationProvider.java index cb9ed54e5d279..5a32623a5c4b3 100644 --- a/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinCompilationProvider.java +++ b/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinCompilationProvider.java @@ -19,7 +19,7 @@ import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler; import org.jetbrains.kotlin.config.Services; -import io.quarkus.dev.CompilationProvider; +import io.quarkus.deployment.dev.CompilationProvider; public class KotlinCompilationProvider implements CompilationProvider { diff --git a/extensions/kotlin/deployment/src/main/resources/META-INF/services/io.quarkus.dev.CompilationProvider b/extensions/kotlin/deployment/src/main/resources/META-INF/services/io.quarkus.deployment.dev.CompilationProvider similarity index 100% rename from extensions/kotlin/deployment/src/main/resources/META-INF/services/io.quarkus.dev.CompilationProvider rename to extensions/kotlin/deployment/src/main/resources/META-INF/services/io.quarkus.deployment.dev.CompilationProvider diff --git a/extensions/scala/deployment/pom.xml b/extensions/scala/deployment/pom.xml index 1de5ed080bc47..4855bebbdcbdb 100644 --- a/extensions/scala/deployment/pom.xml +++ b/extensions/scala/deployment/pom.xml @@ -24,11 +24,6 @@ quarkus-jackson-spi - - io.quarkus - quarkus-development-mode - - org.scala-lang scala-compiler diff --git a/extensions/scala/deployment/src/main/java/io/quarkus/scala/deployment/ScalaCompilationProvider.java b/extensions/scala/deployment/src/main/java/io/quarkus/scala/deployment/ScalaCompilationProvider.java index 027f14ebb2bbf..f5edd1faf09e4 100644 --- a/extensions/scala/deployment/src/main/java/io/quarkus/scala/deployment/ScalaCompilationProvider.java +++ b/extensions/scala/deployment/src/main/java/io/quarkus/scala/deployment/ScalaCompilationProvider.java @@ -6,7 +6,7 @@ import java.util.Set; import java.util.stream.Collectors; -import io.quarkus.dev.CompilationProvider; +import io.quarkus.deployment.dev.CompilationProvider; import scala.collection.JavaConverters; import scala.tools.nsc.Global; import scala.tools.nsc.Settings; diff --git a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java index e95218d0d141e..955231ae65e50 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java @@ -205,6 +205,12 @@ void validateFaultToleranceAnnotations(SmallryeFaultToleranceRecorder recorder, } private boolean hasFTAnnotations(IndexView index, AnnotationStore annotationStore, ClassInfo info) { + if (info == null) { + //should not happen, but guard against it + //happens in this case due to a bug involving array types + + return false; + } // first check annotations on type if (annotationStore.hasAnyAnnotation(info, FT_ANNOTATIONS)) { return true; diff --git a/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java b/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java index 7874d5c02408a..d2157cf81992e 100644 --- a/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java +++ b/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java @@ -397,7 +397,11 @@ void registerMetricsFromProducers( BeanArchiveIndexBuildItem beanArchiveIndex) { IndexView index = beanArchiveIndex.getIndex(); for (io.quarkus.arc.processor.BeanInfo bean : validationPhase.getContext().beans().producers()) { - MetricType metricType = getMetricType(bean.getImplClazz()); + ClassInfo implClazz = bean.getImplClazz(); + if (implClazz == null) { + continue; + } + MetricType metricType = getMetricType(implClazz); if (metricType != null) { AnnotationTarget target = bean.getTarget().get(); AnnotationInstance metricAnnotation = null; diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/StartupAction.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/StartupAction.java index 7712455f96696..d171fbfe1cf66 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/StartupAction.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/StartupAction.java @@ -1,5 +1,18 @@ package io.quarkus.bootstrap.app; +import java.util.function.Consumer; + public interface StartupAction { - public RunningQuarkusApplication run(String... args) throws Exception; + + /** + * Runs the application by running the main method of the main class. As this is a blocking method a new + * thread is created to run this task. + * + * Before this method is called an appropriate exit handler will likely need to + * be set in {@link io.quarkus.runtime.ApplicationLifecycleManager#setDefaultExitCodeHandler(Consumer)} + * of the JVM will exit when the app stops. + */ + RunningQuarkusApplication runMainClass(String... args) throws Exception; + + RunningQuarkusApplication run(String... args) throws Exception; } diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/model/AppModel.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/model/AppModel.java index 037310bbe7d69..7ff7b0ca63fb2 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/model/AppModel.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/model/AppModel.java @@ -175,9 +175,16 @@ public void handleExtensionProperties(Properties props, String extension) { } public AppModel build() { - Predicate includePredicate = s -> !excludedArtifacts - .contains(new AppArtifactKey(s.getArtifact().getGroupId(), s.getArtifact().getArtifactId(), - s.getArtifact().getClassifier(), s.getArtifact().getType())); + Predicate includePredicate = s -> { + //we never include the ide launcher in the final app model + if (s.getArtifact().getGroupId().equals("io.quarkus") + && s.getArtifact().getArtifactId().equals("quarkus-ide-launcher")) { + return false; + } + return !excludedArtifacts + .contains(new AppArtifactKey(s.getArtifact().getGroupId(), s.getArtifact().getArtifactId(), + s.getArtifact().getClassifier(), s.getArtifact().getType())); + }; List runtimeDeps = this.runtimeDeps.stream().filter(includePredicate).collect(Collectors.toList()); List deploymentDeps = this.deploymentDeps.stream().filter(includePredicate) .collect(Collectors.toList()); diff --git a/integration-tests/hibernate-validator/src/main/java/io/quarkus/it/hibernate/validator/Main.java b/integration-tests/hibernate-validator/src/main/java/io/quarkus/it/hibernate/validator/Main.java new file mode 100644 index 0000000000000..32412c7483753 --- /dev/null +++ b/integration-tests/hibernate-validator/src/main/java/io/quarkus/it/hibernate/validator/Main.java @@ -0,0 +1,10 @@ +package io.quarkus.it.hibernate.validator; + +import io.quarkus.runtime.Quarkus; + +public class Main { + + public static void main(String... args) throws Exception { + Quarkus.run(args); + } +} diff --git a/test-framework/junit5-internal/pom.xml b/test-framework/junit5-internal/pom.xml index f950897a0a153..2e1a34b24a812 100644 --- a/test-framework/junit5-internal/pom.xml +++ b/test-framework/junit5-internal/pom.xml @@ -52,10 +52,6 @@ jakarta.enterprise jakarta.enterprise.cdi-api - - io.quarkus - quarkus-development-mode - io.quarkus quarkus-devtools-utilities diff --git a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusDevModeTest.java b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusDevModeTest.java index 8ceb0414b72cf..cb39af6301c96 100644 --- a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusDevModeTest.java +++ b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusDevModeTest.java @@ -31,10 +31,11 @@ import org.junit.jupiter.api.extension.TestInstanceFactoryContext; import org.junit.jupiter.api.extension.TestInstantiationException; +import io.quarkus.deployment.dev.CompilationProvider; +import io.quarkus.deployment.dev.DevModeContext; +import io.quarkus.deployment.dev.DevModeMain; import io.quarkus.deployment.util.FileUtil; -import io.quarkus.dev.CompilationProvider; -import io.quarkus.dev.DevModeContext; -import io.quarkus.dev.DevModeMain; +import io.quarkus.dev.appstate.ApplicationStateNotification; import io.quarkus.test.common.PathTestHelper; import io.quarkus.test.common.PropertyTestUtil; import io.quarkus.test.common.TestResourceManager; @@ -149,6 +150,7 @@ public void close() throws Throwable { context.setAbortOnFailedStart(true); devModeMain = new DevModeMain(context); devModeMain.start(); + ApplicationStateNotification.waitForApplicationStart(); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusProdModeTest.java b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusProdModeTest.java index 754bd059830d8..a4f56ded94c32 100644 --- a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusProdModeTest.java +++ b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusProdModeTest.java @@ -87,6 +87,9 @@ public class QuarkusProdModeTest private Path logfilePath; private Optional logfileField = Optional.empty(); private List forcedDependencies = Collections.emptyList(); + private boolean expectExit; + private String startupConsoleOutput; + private int exitCode; public Supplier getArchiveProducer() { return archiveProducer; @@ -160,6 +163,29 @@ public QuarkusProdModeTest setForcedDependencies(List forcedDepende return this; } + /** + * If this is true then the quarkus application is expected to exit immediately (i.e. is a command mode app) + */ + public QuarkusProdModeTest setExpectExit(boolean expectExit) { + this.expectExit = expectExit; + return this; + } + + /** + * Returns the console output from startup. If {@link #expectExit} is true then this will contain + * all the console output. + */ + public String getStartupConsoleOutput() { + return startupConsoleOutput; + } + + /** + * Returns the process exit code, this can only be used if {@link #expectExit} is true + */ + public int getExitCode() { + return exitCode; + } + private void exportArchive(Path deploymentDir, Class testClass) { try { JavaArchive archive = getArchiveProducerOrDefault(); @@ -334,19 +360,33 @@ private void startBuiltResult(Path builtResultArtifact) throws IOException { private void ensureApplicationStartupOrFailure() throws IOException { BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); while (true) { - if (!process.isAlive()) { - in.close(); - throw new RuntimeException( - "The produced jar could not be launched. Consult the above output for the exact cause."); - } String line = in.readLine(); if (line != null) { System.out.println(line); - if (line.contains(EXPECTED_OUTPUT_FROM_SUCCESSFULLY_STARTED)) { + sb.append(line); + sb.append("\n"); + if (!expectExit && line.contains(EXPECTED_OUTPUT_FROM_SUCCESSFULLY_STARTED)) { in.close(); + this.startupConsoleOutput = sb.toString(); break; } + } else { + //process has exited + this.startupConsoleOutput = sb.toString(); + in.close(); + try { + process.waitFor(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + exitCode = process.exitValue(); + if (expectExit) { + return; + } + throw new RuntimeException( + "The produced jar could not be launched. Consult the above output for the exact cause."); } } } diff --git a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java index f03286d9aab12..c0bc78790f2af 100644 --- a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java +++ b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java @@ -93,6 +93,7 @@ public class QuarkusUnitTest private Class actualTestClass; private Object actualTestInstance; + private String[] commandLineParameters = new String[0]; private boolean allowTestClassOutsideDeployment; @@ -174,6 +175,15 @@ public QuarkusUnitTest setForcedDependencies(List forcedDependencie return this; } + public String[] getCommandLineParameters() { + return commandLineParameters; + } + + public QuarkusUnitTest setCommandLineParameters(String... commandLineParameters) { + this.commandLineParameters = commandLineParameters; + return this; + } + /** * Normally access to any test classes that are not packaged in the deployment will result * in a ClassNotFoundException. If this is true then access is allowed, which can be useful @@ -387,7 +397,7 @@ public void execute(BuildContext context) { runningQuarkusApplication = new AugmentActionImpl(curatedApplication, customizers) .createInitialRuntimeApplication() - .run(new String[0]); + .run(commandLineParameters); //we restore the CL at the end of the test Thread.currentThread().setContextClassLoader(runningQuarkusApplication.getClassLoader()); if (assertException != null) {