From 01547275b5583bd4198e10ae141f6f99db501aa8 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 5 Mar 2019 09:41:22 +0200 Subject: [PATCH] Add very basic support for command line only applications Relates to: #284 --- bom/pom.xml | 6 + build-parent/pom.xml | 10 ++ .../quarkus/deployment/ExtensionLoader.java | 24 +++- .../quarkus/deployment/QuarkusAugmentor.java | 3 + .../deployment/annotations/ExecutionTime.java | 7 +- ...AfterStartupBytecodeRecorderBuildItem.java | 18 +++ .../builditem/MainArgsBuildItem.java | 23 ++++ .../builditem/ShutdownBuildItem.java | 6 + .../deployment/steps/MainClassBuildStep.java | 31 +++++ .../java/io/quarkus/runtime/Application.java | 9 ++ .../io/quarkus/runtime/MainArgsSupplier.java | 6 + .../io/quarkus/runtime/StartupContext.java | 9 ++ .../command-line-runner/deployment/pom.xml | 61 ++++++++++ .../CommandLineRunnerBuildItem.java | 16 +++ .../CommandLineRunnerProcessor.java | 68 +++++++++++ extensions/command-line-runner/pom.xml | 37 ++++++ .../command-line-runner/runtime/pom.xml | 60 +++++++++ .../quarkus/clrunner/CommandLineRunner.java | 6 + .../runtime/CommandLineRunnerTemplate.java | 25 ++++ extensions/pom.xml | 2 + integration-tests/command-line-runner/pom.xml | 103 ++++++++++++++++ .../clrunner/tests/CommandLineRunnerIT.java | 70 +++++++++++ .../support/MavenProcessInvocationResult.java | 45 +++++++ .../tests/support/MavenProcessInvoker.java | 115 ++++++++++++++++++ .../clrunner/tests/support/MojoTestBase.java | 86 +++++++++++++ .../tests/support/RunningInvoker.java | 74 +++++++++++ .../test/resources/projects/classic/pom.xml | 62 ++++++++++ .../src/main/java/org/acme/Capitalizer.java | 13 ++ .../src/main/java/org/acme/ExampleRunner.java | 40 ++++++ .../META-INF/microprofile-config.properties | 3 + integration-tests/pom.xml | 2 + 31 files changed, 1034 insertions(+), 6 deletions(-) create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/builditem/MainAfterStartupBytecodeRecorderBuildItem.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/builditem/MainArgsBuildItem.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/builditem/ShutdownBuildItem.java create mode 100644 core/runtime/src/main/java/io/quarkus/runtime/MainArgsSupplier.java create mode 100644 extensions/command-line-runner/deployment/pom.xml create mode 100644 extensions/command-line-runner/deployment/src/main/java/io/quarkus/clrunner/deployment/CommandLineRunnerBuildItem.java create mode 100644 extensions/command-line-runner/deployment/src/main/java/io/quarkus/clrunner/deployment/CommandLineRunnerProcessor.java create mode 100644 extensions/command-line-runner/pom.xml create mode 100644 extensions/command-line-runner/runtime/pom.xml create mode 100644 extensions/command-line-runner/runtime/src/main/java/io/quarkus/clrunner/CommandLineRunner.java create mode 100644 extensions/command-line-runner/runtime/src/main/java/io/quarkus/clrunner/runtime/CommandLineRunnerTemplate.java create mode 100644 integration-tests/command-line-runner/pom.xml create mode 100644 integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/CommandLineRunnerIT.java create mode 100644 integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/support/MavenProcessInvocationResult.java create mode 100644 integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/support/MavenProcessInvoker.java create mode 100644 integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/support/MojoTestBase.java create mode 100644 integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/support/RunningInvoker.java create mode 100644 integration-tests/command-line-runner/src/test/resources/projects/classic/pom.xml create mode 100644 integration-tests/command-line-runner/src/test/resources/projects/classic/src/main/java/org/acme/Capitalizer.java create mode 100644 integration-tests/command-line-runner/src/test/resources/projects/classic/src/main/java/org/acme/ExampleRunner.java create mode 100644 integration-tests/command-line-runner/src/test/resources/projects/classic/src/main/resources/META-INF/microprofile-config.properties diff --git a/bom/pom.xml b/bom/pom.xml index 8c17bb94f3c89..864ed13d74bb0 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -419,6 +419,12 @@ ${project.version} provided + + io.quarkus + quarkus-command-line-runner + ${project.version} + provided + diff --git a/build-parent/pom.xml b/build-parent/pom.xml index c592236aa0c64..67a858b648432 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -541,6 +541,16 @@ quarkus-legacy-launcher ${project.version} + + io.quarkus + quarkus-command-line-runner + ${project.version} + + + io.quarkus + quarkus-command-line-runner-runtime + ${project.version} + diff --git a/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java b/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java index 41d7f6d2aba9a..7c2f3c61a623f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java @@ -51,6 +51,7 @@ import io.quarkus.deployment.builditem.AdditionalApplicationArchiveMarkerBuildItem; import io.quarkus.deployment.builditem.CapabilityBuildItem; import io.quarkus.deployment.builditem.ConfigurationBuildItem; +import io.quarkus.deployment.builditem.MainAfterStartupBytecodeRecorderBuildItem; import io.quarkus.deployment.builditem.MainBytecodeRecorderBuildItem; import io.quarkus.deployment.builditem.StaticBytecodeRecorderBuildItem; import io.quarkus.deployment.recording.BytecodeRecorderImpl; @@ -283,10 +284,21 @@ public static Consumer loadStepsFrom(Class clazz) { assert recordAnnotation != null; final ExecutionTime executionTime = recordAnnotation.value(); final boolean optional = recordAnnotation.optional(); - methodStepConfig = methodStepConfig.andThen(bsb -> bsb.produces( - executionTime == ExecutionTime.STATIC_INIT ? StaticBytecodeRecorderBuildItem.class - : MainBytecodeRecorderBuildItem.class, - optional ? ProduceFlags.of(ProduceFlag.WEAK) : ProduceFlags.NONE)); + methodStepConfig = methodStepConfig.andThen(bsb -> { + Class buildItem = executionTime == ExecutionTime.STATIC_INIT + ? StaticBytecodeRecorderBuildItem.class + : MainBytecodeRecorderBuildItem.class; + if (executionTime == ExecutionTime.STATIC_INIT) { + buildItem = StaticBytecodeRecorderBuildItem.class; + } else if (executionTime == ExecutionTime.RUNTIME_INIT) { + buildItem = MainBytecodeRecorderBuildItem.class; + } else if (executionTime == ExecutionTime.AFTER_STARTUP) { + buildItem = MainAfterStartupBytecodeRecorderBuildItem.class; + } + bsb.produces( + buildItem, + optional ? ProduceFlags.of(ProduceFlag.WEAK) : ProduceFlags.NONE); + }); } boolean methodConsumingConfig = consumingConfig; if (methodParameters.length == 0) { @@ -449,8 +461,10 @@ public void execute(final BuildContext bc) { // commit recorded data if (recordAnnotation.value() == ExecutionTime.STATIC_INIT) { bc.produce(new StaticBytecodeRecorderBuildItem(bri)); - } else { + } else if (recordAnnotation.value() == ExecutionTime.RUNTIME_INIT) { bc.produce(new MainBytecodeRecorderBuildItem(bri)); + } else if (recordAnnotation.value() == ExecutionTime.AFTER_STARTUP) { + bc.produce(new MainAfterStartupBytecodeRecorderBuildItem(bri)); } } 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 7bc6f6efedd03..b34a67347d6b9 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java @@ -38,6 +38,7 @@ import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.GeneratedResourceBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.builditem.MainArgsBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.substrate.SubstrateResourceBuildItem; import io.quarkus.runtime.LaunchMode; @@ -81,6 +82,7 @@ public BuildResult run() throws Exception { .addInitial(SubstrateResourceBuildItem.class) .addInitial(ArchiveRootBuildItem.class) .addInitial(ShutdownContextBuildItem.class) + .addInitial(MainArgsBuildItem.class) .addInitial(ClassOutputBuildItem.class) .addInitial(LaunchModeBuildItem.class) .addInitial(AdditionalApplicationArchiveBuildItem.class) @@ -104,6 +106,7 @@ public BuildResult run() throws Exception { .produce(new ArchiveRootBuildItem(root)) .produce(new ClassOutputBuildItem(output)) .produce(new ShutdownContextBuildItem()) + .produce(new MainArgsBuildItem()) .produce(new LaunchModeBuildItem(launchMode)) .produce(new ExtensionClassLoaderBuildItem(classLoader)); for (Path i : additionalApplicationArchives) { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/annotations/ExecutionTime.java b/core/deployment/src/main/java/io/quarkus/deployment/annotations/ExecutionTime.java index dcaf9981159a7..c37a3544fe669 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/annotations/ExecutionTime.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/annotations/ExecutionTime.java @@ -27,5 +27,10 @@ public enum ExecutionTime { /** * The bytecode is run from a main method */ - RUNTIME_INIT + RUNTIME_INIT, + + /** + * The bytecode is run from a main method after all RUNTIME_INIT + */ + AFTER_STARTUP } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/MainAfterStartupBytecodeRecorderBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/MainAfterStartupBytecodeRecorderBuildItem.java new file mode 100644 index 0000000000000..741fddb458459 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/MainAfterStartupBytecodeRecorderBuildItem.java @@ -0,0 +1,18 @@ +package io.quarkus.deployment.builditem; + +import org.jboss.builder.item.MultiBuildItem; + +import io.quarkus.deployment.recording.BytecodeRecorderImpl; + +public final class MainAfterStartupBytecodeRecorderBuildItem extends MultiBuildItem { + + private final BytecodeRecorderImpl bytecodeRecorder; + + public MainAfterStartupBytecodeRecorderBuildItem(BytecodeRecorderImpl bytecodeRecorder) { + this.bytecodeRecorder = bytecodeRecorder; + } + + public BytecodeRecorderImpl getBytecodeRecorder() { + return bytecodeRecorder; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/MainArgsBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/MainArgsBuildItem.java new file mode 100644 index 0000000000000..d111954bfbe73 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/MainArgsBuildItem.java @@ -0,0 +1,23 @@ +package io.quarkus.deployment.builditem; + +import org.jboss.builder.item.SimpleBuildItem; + +import io.quarkus.deployment.recording.BytecodeRecorderImpl; +import io.quarkus.runtime.MainArgsSupplier; + +public final class MainArgsBuildItem extends SimpleBuildItem implements BytecodeRecorderImpl.ReturnedProxy, MainArgsSupplier { + @Override + public String __returned$proxy$key() { + return MainArgsSupplier.class.getName(); + } + + @Override + public boolean __static$$init() { + return true; + } + + @Override + public String[] getArgs() { + throw new IllegalStateException(); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/ShutdownBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/ShutdownBuildItem.java new file mode 100644 index 0000000000000..ba6b10bc1b84e --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/ShutdownBuildItem.java @@ -0,0 +1,6 @@ +package io.quarkus.deployment.builditem; + +import org.jboss.builder.item.SimpleBuildItem; + +public final class ShutdownBuildItem extends SimpleBuildItem { +} 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 59be7ea0fccdc..c9785dcc023ca 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 @@ -36,9 +36,11 @@ import io.quarkus.deployment.builditem.ClassOutputBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.JavaLibraryPathAdditionalPathBuildItem; +import io.quarkus.deployment.builditem.MainAfterStartupBytecodeRecorderBuildItem; import io.quarkus.deployment.builditem.MainBytecodeRecorderBuildItem; import io.quarkus.deployment.builditem.MainClassBuildItem; import io.quarkus.deployment.builditem.ObjectSubstitutionBuildItem; +import io.quarkus.deployment.builditem.ShutdownBuildItem; import io.quarkus.deployment.builditem.SslTrustStoreSystemPropertyBuildItem; import io.quarkus.deployment.builditem.StaticBytecodeRecorderBuildItem; import io.quarkus.deployment.builditem.SystemPropertyBuildItem; @@ -70,12 +72,14 @@ class MainClassBuildStep { MainClassBuildItem build(List staticInitTasks, List substitutions, List mainMethod, + List mainMethodAfterStartup, List properties, List javaLibraryPathAdditionalPaths, Optional sslTrustStoreSystemProperty, List features, BuildProducer appClassNameProducer, List loaders, + Optional shutdownBuildItem, ClassOutputBuildItem classOutput) { String appClassName = APP_CLASS + COUNT.incrementAndGet(); @@ -131,6 +135,7 @@ MainClassBuildItem build(List staticInitTasks, mv = file.getMethodCreator("doStart", void.class, String[].class); mv.setModifiers(Modifier.PROTECTED | Modifier.FINAL); + ResultHandle mainMethodArgs = mv.getMethodParam(0); // very first thing is to set system props (for run time, which use substitutions for a different // storage from build-time) @@ -187,6 +192,9 @@ MainClassBuildItem build(List staticInitTasks, mv.invokeStaticMethod(ofMethod(Timing.class, "mainStarted", void.class)); startupContext = mv.readStaticField(scField.getFieldDescriptor()); + mv.invokeVirtualMethod(ofMethod(StartupContext.class, "setMainArgs", void.class, String[].class), + startupContext, mainMethodArgs); + tryBlock = mv.tryBlock(); for (MainBytecodeRecorderBuildItem holder : mainMethod) { final BytecodeRecorderImpl recorder = holder.getBytecodeRecorder(); @@ -214,8 +222,31 @@ MainClassBuildItem build(List staticInitTasks, cb.invokeVirtualMethod(ofMethod(Throwable.class, "printStackTrace", void.class), cb.getCaughtException()); cb.invokeVirtualMethod(ofMethod(StartupContext.class, "close", void.class), startupContext); cb.throwException(RuntimeException.class, "Failed to start quarkus", cb.getCaughtException()); + + // Application.class: start method after startup + for (MainAfterStartupBytecodeRecorderBuildItem holder : mainMethodAfterStartup) { + final BytecodeRecorderImpl recorder = holder.getBytecodeRecorder(); + if (!recorder.isEmpty()) { + for (BytecodeRecorderObjectLoaderBuildItem item : loaders) { + recorder.registerObjectLoader(item.getObjectLoader()); + } + recorder.writeBytecode(classOutput.getClassOutput()); + ResultHandle dup = mv.newInstance(ofConstructor(recorder.getClassName())); + mv.invokeInterfaceMethod(ofMethod(StartupTask.class, "deploy", void.class, StartupContext.class), dup, + startupContext); + } + } + mv.returnValue(null); + // Application class: doPostStart method + if (shutdownBuildItem.isPresent()) { + mv = file.getMethodCreator("doPostStart", void.class); + mv.setModifiers(Modifier.PROTECTED); + mv.invokeSpecialMethod(ofMethod(Application.class, "requestShutdown", void.class), mv.getThis()); + mv.returnValue(null); + } + // Application class: stop method mv = file.getMethodCreator("doStop", void.class); 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 877ccade2678f..f3444a1eacb32 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/Application.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/Application.java @@ -108,10 +108,15 @@ public final void start(@SuppressWarnings("unused") String[] args) { } finally { stateLock.unlock(); } + doPostStart(); } protected abstract void doStart(String[] args); + protected void doPostStart() { + + } + /** * Stop the application. If another thread is also trying to stop the application, this method waits for that * thread to finish. Returns immediately if the application is already stopped. If an exception is thrown during @@ -222,6 +227,10 @@ private void exit() { } } + protected void requestShutdown() { + shutdownRequested = true; + } + private static IllegalStateException interruptedOnAwaitStart() { return new IllegalStateException("Interrupted while waiting for another thread to start the application"); } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/MainArgsSupplier.java b/core/runtime/src/main/java/io/quarkus/runtime/MainArgsSupplier.java new file mode 100644 index 0000000000000..9cd724c713222 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/MainArgsSupplier.java @@ -0,0 +1,6 @@ +package io.quarkus.runtime; + +public interface MainArgsSupplier { + + String[] getArgs(); +} 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 c5a152746514f..da4814d4c5260 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/StartupContext.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/StartupContext.java @@ -42,6 +42,15 @@ public StartupContext() { values.put(ShutdownContext.class.getName(), shutdownContext); } + public void setMainArgs(final String[] args) { + values.put(MainArgsSupplier.class.getName(), new MainArgsSupplier() { + @Override + public String[] getArgs() { + return args; + } + }); + } + public void putValue(String name, Object value) { values.put(name, value); } diff --git a/extensions/command-line-runner/deployment/pom.xml b/extensions/command-line-runner/deployment/pom.xml new file mode 100644 index 0000000000000..a6c8658849f12 --- /dev/null +++ b/extensions/command-line-runner/deployment/pom.xml @@ -0,0 +1,61 @@ + + + + + quarkus-command-line-runner-parent + io.quarkus + 999-SNAPSHOT + ../ + + + 4.0.0 + + quarkus-command-line-runner + Quarkus - Command Line Runner - Deployment + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-command-line-runner-runtime + + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + diff --git a/extensions/command-line-runner/deployment/src/main/java/io/quarkus/clrunner/deployment/CommandLineRunnerBuildItem.java b/extensions/command-line-runner/deployment/src/main/java/io/quarkus/clrunner/deployment/CommandLineRunnerBuildItem.java new file mode 100644 index 0000000000000..aa9355814995e --- /dev/null +++ b/extensions/command-line-runner/deployment/src/main/java/io/quarkus/clrunner/deployment/CommandLineRunnerBuildItem.java @@ -0,0 +1,16 @@ +package io.quarkus.clrunner.deployment; + +import org.jboss.builder.item.MultiBuildItem; + +public final class CommandLineRunnerBuildItem extends MultiBuildItem { + + private final String className; + + public CommandLineRunnerBuildItem(String className) { + this.className = className; + } + + public String getClassName() { + return className; + } +} diff --git a/extensions/command-line-runner/deployment/src/main/java/io/quarkus/clrunner/deployment/CommandLineRunnerProcessor.java b/extensions/command-line-runner/deployment/src/main/java/io/quarkus/clrunner/deployment/CommandLineRunnerProcessor.java new file mode 100644 index 0000000000000..ee3f2f53f7803 --- /dev/null +++ b/extensions/command-line-runner/deployment/src/main/java/io/quarkus/clrunner/deployment/CommandLineRunnerProcessor.java @@ -0,0 +1,68 @@ +package io.quarkus.clrunner.deployment; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.BeanContainerBuildItem; +import io.quarkus.clrunner.CommandLineRunner; +import io.quarkus.clrunner.runtime.CommandLineRunnerTemplate; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.MainArgsBuildItem; +import io.quarkus.deployment.builditem.ShutdownBuildItem; +import io.quarkus.deployment.recording.RecorderContext; + +public class CommandLineRunnerProcessor { + + private static final DotName COMMAND_LINER_RUNNER = DotName.createSimple(CommandLineRunner.class.getName()); + + @BuildStep + List discover(CombinedIndexBuildItem combinedIndexBuildItem) { + final List result = new ArrayList<>(); + + for (ClassInfo info : combinedIndexBuildItem.getIndex().getAllKnownImplementors(COMMAND_LINER_RUNNER)) { + final DotName name = info.name(); + + result.add(new CommandLineRunnerBuildItem(name.toString())); + } + return result; + } + + @BuildStep + List beans(List runners) { + final List result = new ArrayList<>(); + for (CommandLineRunnerBuildItem runner : runners) { + result.add(new AdditionalBeanBuildItem(false, runner.getClassName())); + } + return result; + } + + @BuildStep + ShutdownBuildItem shutdown() { + // TODO: this has to be conditional only set when various things are not present + return new ShutdownBuildItem(); + } + + @BuildStep + @Record(ExecutionTime.AFTER_STARTUP) + public void runners(List runners, + BeanContainerBuildItem beanContainerBuildItem, + MainArgsBuildItem mainArgsBuildItem, + CommandLineRunnerTemplate template, + RecorderContext context) { + + Set> runnerClasses = new HashSet<>(); + for (CommandLineRunnerBuildItem runner : runners) { + runnerClasses.add((Class) context.classProxy(runner.getClassName())); + } + template.run(runnerClasses, beanContainerBuildItem.getValue(), mainArgsBuildItem); + } +} diff --git a/extensions/command-line-runner/pom.xml b/extensions/command-line-runner/pom.xml new file mode 100644 index 0000000000000..ea88743a770c4 --- /dev/null +++ b/extensions/command-line-runner/pom.xml @@ -0,0 +1,37 @@ + + + + + + quarkus-build-parent + io.quarkus + 999-SNAPSHOT + ../../build-parent/pom.xml + + 4.0.0 + + quarkus-command-line-runner-parent + Quarkus - Command Line Runner + pom + + deployment + runtime + + + diff --git a/extensions/command-line-runner/runtime/pom.xml b/extensions/command-line-runner/runtime/pom.xml new file mode 100644 index 0000000000000..1459f21ada6a0 --- /dev/null +++ b/extensions/command-line-runner/runtime/pom.xml @@ -0,0 +1,60 @@ + + + + + + quarkus-command-line-runner-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-command-line-runner-runtime + Quarkus - Command Line Runner - Runtime + + + + + io.quarkus + quarkus-arc-runtime + + + + + + + + maven-dependency-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/command-line-runner/runtime/src/main/java/io/quarkus/clrunner/CommandLineRunner.java b/extensions/command-line-runner/runtime/src/main/java/io/quarkus/clrunner/CommandLineRunner.java new file mode 100644 index 0000000000000..88a0607c179d5 --- /dev/null +++ b/extensions/command-line-runner/runtime/src/main/java/io/quarkus/clrunner/CommandLineRunner.java @@ -0,0 +1,6 @@ +package io.quarkus.clrunner; + +public interface CommandLineRunner { + + void run(String[] args); +} diff --git a/extensions/command-line-runner/runtime/src/main/java/io/quarkus/clrunner/runtime/CommandLineRunnerTemplate.java b/extensions/command-line-runner/runtime/src/main/java/io/quarkus/clrunner/runtime/CommandLineRunnerTemplate.java new file mode 100644 index 0000000000000..49d5fbd894268 --- /dev/null +++ b/extensions/command-line-runner/runtime/src/main/java/io/quarkus/clrunner/runtime/CommandLineRunnerTemplate.java @@ -0,0 +1,25 @@ +package io.quarkus.clrunner.runtime; + +import java.util.Set; + +import io.quarkus.arc.runtime.BeanContainer; +import io.quarkus.clrunner.CommandLineRunner; +import io.quarkus.runtime.MainArgsSupplier; +import io.quarkus.runtime.annotations.Template; + +@Template +public class CommandLineRunnerTemplate { + + public void run(Set> handlerClasses, BeanContainer beanContainer, + MainArgsSupplier mainArgsSupplier) { + for (Class handlerClass : handlerClasses) { + final BeanContainer.Factory factory = beanContainer.instanceFactory(handlerClass); + final BeanContainer.Instance instance = factory.create(); + final CommandLineRunner commandLineRunner = instance.get(); + + commandLineRunner.run(mainArgsSupplier.getArgs()); + + instance.close(); + } + } +} diff --git a/extensions/pom.xml b/extensions/pom.xml index 14eacc9a26c88..a1ac1bc20b35c 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -87,6 +87,8 @@ camel kotlin + + command-line-runner diff --git a/integration-tests/command-line-runner/pom.xml b/integration-tests/command-line-runner/pom.xml new file mode 100644 index 0000000000000..8296ebe31e921 --- /dev/null +++ b/integration-tests/command-line-runner/pom.xml @@ -0,0 +1,103 @@ + + + + 4.0.0 + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + ../ + + + quarkus-integration-test-command-line-runner + Quarkus - Integration Tests - CLIR + + + + io.quarkus + quarkus-command-line-runner + provided + + + + + io.quarkus + quarkus-devtools-common + test + + + org.junit.jupiter + junit-jupiter-api + + + org.junit.jupiter + junit-jupiter-params + + + org.junit.jupiter + junit-jupiter-engine + + + org.assertj + assertj-core + + + org.apache.maven.shared + maven-invoker + test + + + org.awaitility + awaitility + test + + + org.jprocesses + jProcesses + test + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + + + ${maven.home} + ${settings.localRepository} + ${project.version} + + + + + + + diff --git a/integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/CommandLineRunnerIT.java b/integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/CommandLineRunnerIT.java new file mode 100644 index 0000000000000..2f5440e7d6cd6 --- /dev/null +++ b/integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/CommandLineRunnerIT.java @@ -0,0 +1,70 @@ +package io.quarkus.clrunner.tests; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.maven.shared.invoker.MavenInvocationException; +import org.junit.jupiter.api.Test; + +import io.quarkus.clrunner.tests.support.MavenProcessInvocationResult; +import io.quarkus.clrunner.tests.support.MojoTestBase; +import io.quarkus.clrunner.tests.support.RunningInvoker; + +public class CommandLineRunnerIT extends MojoTestBase { + + private RunningInvoker running; + private File testDir; + + @Test + public void testCommandLineRunnerIsBuiltProperlyAndExecutesAsExpected() + throws MavenInvocationException, IOException, InterruptedException { + // copy the template + testDir = initProject("projects/classic", "projects/project-classic-clir"); + + // invoke the build + running = new RunningInvoker(testDir, false); + final MavenProcessInvocationResult result = running.execute(Collections.singletonList("package"), + Collections.emptyMap()); + + // ensure the build completed successfully + assertThat(result.getProcess().waitFor()).isEqualTo(0); + + // ensure the runner jar was created + final File targetDir = getTargetDir(); + final List runnerJarFiles = getFilesEndingWith(targetDir, "-runner.jar"); + assertThat(runnerJarFiles).hasSize(1); + + final File runnerJarFile = runnerJarFiles.get(0); + final String expectedOutputFile = runnerJarFile.getParentFile().getAbsolutePath() + "/dummy.txt"; + + // launch the jar we just created by passing the file we expect it write and the some input we expect it to capitalize and place in the output + final Process process = new ProcessBuilder() + .directory(runnerJarFile.getParentFile()) + .command("java", "-jar", runnerJarFile.getAbsolutePath(), expectedOutputFile, "a", "b", "c") + .start(); + + // ensure the application completed successfully + assertThat(process.waitFor()).isEqualTo(0); + + // ensure it created the expected output + final String lineSep = System.lineSeparator(); + assertThat(new File(expectedOutputFile)) + .exists() + .hasContent("A" + lineSep + "B" + lineSep + "C" + lineSep); + } + + private File getTargetDir() { + return new File(testDir.getAbsoluteFile() + "/target"); + } + + private List getFilesEndingWith(File dir, String suffix) { + final File[] files = dir.listFiles((d, name) -> name.endsWith(suffix)); + return files != null ? Arrays.asList(files) : new ArrayList<>(); + } +} diff --git a/integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/support/MavenProcessInvocationResult.java b/integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/support/MavenProcessInvocationResult.java new file mode 100644 index 0000000000000..2feb27e82dc58 --- /dev/null +++ b/integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/support/MavenProcessInvocationResult.java @@ -0,0 +1,45 @@ +package io.quarkus.clrunner.tests.support; + +import org.apache.maven.shared.invoker.InvocationResult; +import org.apache.maven.shared.utils.cli.CommandLineException; + +/* + * Copied and adapted from devtools-maven + */ +public class MavenProcessInvocationResult implements InvocationResult { + + private Process process; + private CommandLineException exception; + + MavenProcessInvocationResult setProcess(Process process) { + this.process = process; + return this; + } + + public MavenProcessInvocationResult setException(CommandLineException exception) { + // Print the stack trace immediately to give some feedback early + // In intellij, the used `mvn` executable is not "executable" by default on Mac and probably linux. + // You need to chmod +x the file. + exception.printStackTrace(); + this.exception = exception; + return this; + } + + @Override + public CommandLineException getExecutionException() { + return exception; + } + + @Override + public int getExitCode() { + if (process == null) { + throw new IllegalStateException("No process"); + } else { + return process.exitValue(); + } + } + + public Process getProcess() { + return process; + } +} diff --git a/integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/support/MavenProcessInvoker.java b/integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/support/MavenProcessInvoker.java new file mode 100644 index 0000000000000..172180c3ad46e --- /dev/null +++ b/integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/support/MavenProcessInvoker.java @@ -0,0 +1,115 @@ +package io.quarkus.clrunner.tests.support; + +import java.io.File; + +import org.apache.maven.shared.invoker.CommandLineConfigurationException; +import org.apache.maven.shared.invoker.DefaultInvoker; +import org.apache.maven.shared.invoker.InvocationOutputHandler; +import org.apache.maven.shared.invoker.InvocationRequest; +import org.apache.maven.shared.invoker.InvocationResult; +import org.apache.maven.shared.invoker.InvokerLogger; +import org.apache.maven.shared.invoker.MavenCommandLineBuilder; +import org.apache.maven.shared.invoker.MavenInvocationException; +import org.apache.maven.shared.invoker.SystemOutHandler; +import org.apache.maven.shared.utils.cli.CommandLineException; +import org.apache.maven.shared.utils.cli.Commandline; +import org.apache.maven.shared.utils.cli.StreamConsumer; +import org.apache.maven.shared.utils.cli.StreamPumper; + +/* + * Copied from devtools-maven + */ +public class MavenProcessInvoker extends DefaultInvoker { + + private static final InvocationOutputHandler DEFAULT_OUTPUT_HANDLER = new SystemOutHandler(); + + private InvocationOutputHandler outputHandler = DEFAULT_OUTPUT_HANDLER; + + private InvocationOutputHandler errorHandler = DEFAULT_OUTPUT_HANDLER; + + @Override + public InvocationResult execute(InvocationRequest request) throws MavenInvocationException { + MavenCommandLineBuilder cliBuilder = new MavenCommandLineBuilder(); + + InvokerLogger logger = getLogger(); + if (logger != null) { + cliBuilder.setLogger(getLogger()); + } + + File localRepo = getLocalRepositoryDirectory(); + if (localRepo != null) { + cliBuilder.setLocalRepositoryDirectory(getLocalRepositoryDirectory()); + } + + File mavenHome = getMavenHome(); + if (mavenHome != null) { + cliBuilder.setMavenHome(getMavenHome()); + } + + File mavenExecutable = getMavenExecutable(); + if (mavenExecutable != null) { + cliBuilder.setMavenExecutable(mavenExecutable); + } + + File workingDirectory = getWorkingDirectory(); + if (workingDirectory != null) { + cliBuilder.setWorkingDirectory(getWorkingDirectory()); + } + + request.setBatchMode(true); + + Commandline cli; + try { + cli = cliBuilder.build(request); + } catch (CommandLineConfigurationException e) { + throw new MavenInvocationException("Error configuring command-line. Reason: " + e.getMessage(), e); + } + + MavenProcessInvocationResult result = new MavenProcessInvocationResult(); + + try { + Process process = executeCommandLine(cli, request); + result.setProcess(process); + } catch (CommandLineException e) { + result.setException(e); + } + + return result; + } + + private Process executeCommandLine(Commandline cli, InvocationRequest request) + throws CommandLineException { + InvocationOutputHandler outputHandler = request.getOutputHandler(this.outputHandler); + InvocationOutputHandler errorHandler = request.getErrorHandler(this.errorHandler); + return executeCommandLine(cli, outputHandler, errorHandler); + } + + private static Process executeCommandLine(Commandline cl, StreamConsumer systemOut, StreamConsumer systemErr) + throws CommandLineException { + if (cl == null) { + throw new IllegalArgumentException("the command line cannot be null."); + } else { + final Process p = cl.execute(); + final StreamPumper outputPumper = new StreamPumper(p.getInputStream(), systemOut); + final StreamPumper errorPumper = new StreamPumper(p.getErrorStream(), systemErr); + + outputPumper.start(); + errorPumper.start(); + + new Thread(() -> { + try { + // Wait for termination + p.waitFor(); + outputPumper.waitUntilDone(); + errorPumper.waitUntilDone(); + } catch (Exception e) { + outputPumper.disable(); + errorPumper.disable(); + e.printStackTrace(); + } + }).start(); + + return p; + } + } +} diff --git a/integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/support/MojoTestBase.java b/integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/support/MojoTestBase.java new file mode 100644 index 0000000000000..2a1d1383ac170 --- /dev/null +++ b/integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/support/MojoTestBase.java @@ -0,0 +1,86 @@ +package io.quarkus.clrunner.tests.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.io.File; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.io.FileUtils; +import org.apache.maven.shared.utils.StringUtils; +import org.junit.jupiter.api.BeforeAll; + +import com.google.common.collect.ImmutableMap; + +import io.quarkus.maven.utilities.MojoUtils; + +/* + * Copied and adapted from devtools-maven + */ +public class MojoTestBase { + private static ImmutableMap VARIABLES; + + @BeforeAll + public static void init() { + VARIABLES = ImmutableMap.of( + "@project.groupId@", MojoUtils.getPluginGroupId(), + "@project.artifactId@", MojoUtils.getPluginArtifactId(), + "@project.version@", MojoUtils.getPluginVersion()); + } + + public static File initProject(String name, String output) { + File tc = new File("target/test-classes"); + if (!tc.isDirectory()) { + boolean mkdirs = tc.mkdirs(); + Logger.getLogger(MojoTestBase.class.getName()) + .log(Level.FINE, "test-classes created? " + mkdirs); + } + + File in = new File("src/test/resources", name); + if (!in.isDirectory()) { + throw new RuntimeException("Cannot find directory: " + in.getAbsolutePath()); + } + + File out = new File(tc, output); + if (out.isDirectory()) { + FileUtils.deleteQuietly(out); + } + boolean mkdirs = out.mkdirs(); + Logger.getLogger(MojoTestBase.class.getName()) + .log(Level.FINE, out.getAbsolutePath() + " created? " + mkdirs); + try { + org.codehaus.plexus.util.FileUtils.copyDirectoryStructure(in, out); + } catch (IOException e) { + throw new RuntimeException("Cannot copy project resources", e); + } + + File pom = new File(out, "pom.xml"); + try { + filter(pom, VARIABLES); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + + return out; + } + + static void filter(File input, Map variables) throws IOException { + assertThat(input).isFile(); + String data = FileUtils.readFileToString(input, "UTF-8"); + + for (Map.Entry token : variables.entrySet()) { + String value = String.valueOf(token.getValue()); + data = StringUtils.replace(data, token.getKey(), value); + } + FileUtils.write(input, data, "UTF-8"); + } + +} diff --git a/integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/support/RunningInvoker.java b/integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/support/RunningInvoker.java new file mode 100644 index 0000000000000..e709bb111c1a2 --- /dev/null +++ b/integration-tests/command-line-runner/src/test/java/io/quarkus/clrunner/tests/support/RunningInvoker.java @@ -0,0 +1,74 @@ +package io.quarkus.clrunner.tests.support; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.util.List; +import java.util.Map; + +import org.apache.maven.shared.invoker.DefaultInvocationRequest; +import org.apache.maven.shared.invoker.InvocationRequest; +import org.apache.maven.shared.invoker.InvocationResult; +import org.apache.maven.shared.invoker.InvokerLogger; +import org.apache.maven.shared.invoker.MavenInvocationException; +import org.apache.maven.shared.invoker.PrintStreamHandler; +import org.apache.maven.shared.invoker.PrintStreamLogger; + +/* + * Copied and adapted from devtools-maven + */ +public class RunningInvoker extends MavenProcessInvoker { + + private final boolean debug; + private MavenProcessInvocationResult result; + private final File log; + private final PrintStreamHandler logHandler; + + public RunningInvoker(File basedir, boolean debug) throws FileNotFoundException { + this.debug = debug; + setWorkingDirectory(basedir); + String repo = System.getProperty("maven.repo"); + if (repo == null) { + repo = new File(System.getProperty("user.home"), ".m2/repository").getAbsolutePath(); + } + log = new File(basedir, "build-" + basedir.getName() + ".log"); + PrintStream stream = null; + try { + stream = new PrintStream(log, "UTF-8"); + } catch (UnsupportedEncodingException e) { + stream = new PrintStream(log); + } + logHandler = new PrintStreamHandler(stream, true); + setErrorHandler(logHandler); + setOutputHandler(logHandler); + setLogger(new PrintStreamLogger(stream, InvokerLogger.DEBUG)); + } + + public MavenProcessInvocationResult execute(List goals, Map envVars) + throws MavenInvocationException { + DefaultInvocationRequest request = new DefaultInvocationRequest(); + request.setGoals(goals); + request.setDebug(debug); + request.setLocalRepositoryDirectory(getLocalRepositoryDirectory()); + request.setBaseDirectory(getWorkingDirectory()); + request.setPomFile(new File(getWorkingDirectory(), "pom.xml")); + + if (System.getProperty("mavenOpts") != null) { + request.setMavenOpts(System.getProperty("mavenOpts")); + } + + request.setShellEnvironmentInherited(true); + envVars.forEach(request::addShellEnvironment); + request.setOutputHandler(logHandler); + request.setErrorHandler(logHandler); + this.result = (MavenProcessInvocationResult) execute(request); + return result; + } + + @Override + public InvocationResult execute(InvocationRequest request) throws MavenInvocationException { + return super.execute(request); + } + +} diff --git a/integration-tests/command-line-runner/src/test/resources/projects/classic/pom.xml b/integration-tests/command-line-runner/src/test/resources/projects/classic/pom.xml new file mode 100644 index 0000000000000..18a18a6da4184 --- /dev/null +++ b/integration-tests/command-line-runner/src/test/resources/projects/classic/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + org.acme + acme + 1.0-SNAPSHOT + + @project.version@ + 1.8 + UTF-8 + 1.8 + + + + @project.groupId@ + quarkus-command-line-runner + ${quarkus.version} + provided + + + + + + @project.groupId@ + @project.artifactId@ + ${quarkus.version} + + + + build + + + + + + + + + native + + + + @project.groupId@ + @project.artifactId@ + ${quarkus.version} + + + + native-image + + + true + + + + + + + + + diff --git a/integration-tests/command-line-runner/src/test/resources/projects/classic/src/main/java/org/acme/Capitalizer.java b/integration-tests/command-line-runner/src/test/resources/projects/classic/src/main/java/org/acme/Capitalizer.java new file mode 100644 index 0000000000000..81da4a5537ee2 --- /dev/null +++ b/integration-tests/command-line-runner/src/test/resources/projects/classic/src/main/java/org/acme/Capitalizer.java @@ -0,0 +1,13 @@ +package org.acme; + +import javax.inject.Singleton; + + +@Singleton +public class Capitalizer { + + public String perform(String input) { + return input.toUpperCase(); + } + +} diff --git a/integration-tests/command-line-runner/src/test/resources/projects/classic/src/main/java/org/acme/ExampleRunner.java b/integration-tests/command-line-runner/src/test/resources/projects/classic/src/main/java/org/acme/ExampleRunner.java new file mode 100644 index 0000000000000..abe000aca08b7 --- /dev/null +++ b/integration-tests/command-line-runner/src/test/resources/projects/classic/src/main/java/org/acme/ExampleRunner.java @@ -0,0 +1,40 @@ +package org.acme; + +import io.quarkus.clrunner.CommandLineRunner; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.charset.Charset; + +import javax.inject.Inject; + + + +public class ExampleRunner implements CommandLineRunner { + + @Inject + Capitalizer capitalizer; + + @Override + public void run(String[] args) { + final List lines = new ArrayList<>(); + for (int i=1; ielytron-security camel-core camel-salesforce + --> + command-line-runner