This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-25400-embedded in repository https://gitbox.apache.org/repos/asf/camel.git
commit 156648647c42fb1e10fc893652f6e2fa1055438c Author: Claus Ibsen <[email protected]> AuthorDate: Fri Oct 16 19:19:46 2026 +0200 CAMEL-25400: camel-jbang - The CLI that runs several commands gives each a fresh start camel ask (cli_exec) and the AI panel of the monitor run Camel CLI commands in their own JVM with CliRunner, which reused one command tree: a field of a command that is not an option kept the value of the run before. Each run now gets new instances of the commands (CamelJBangMain.newCommandLine, with the plugins already loaded). log4j was configured once per JVM, so a later command in such a CLI (or in camel shell) that logs otherwise, such as export to its log file, only got its level, and logged to the console. The first logging configuration of a new command is now applied; a command run on its own behaves as before. camel doc and the catalog listings load the catalog of their runtime with one method. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../dsl/jbang/core/commands/CamelJBangMain.java | 108 ++++++++---- .../core/commands/CamelShellCommandRegistry.java | 2 + .../camel/dsl/jbang/core/commands/CliRunner.java | 8 +- .../core/commands/catalog/CatalogBaseCommand.java | 9 + .../jbang/core/commands/catalog/CatalogDoc.java | 21 +-- .../camel/dsl/jbang/core/common/PluginHelper.java | 12 +- .../camel/dsl/jbang/core/common/RuntimeUtil.java | 182 +++++++++++++-------- .../dsl/jbang/core/commands/CliRunnerTest.java | 35 +++- .../jbang/core/common/RuntimeUtilLoggingTest.java | 58 +++++++ .../commands/tui/AiCliCommandExecutorTest.java | 11 +- 10 files changed, 312 insertions(+), 134 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java index 66143a1be5e6..90a71042ec3c 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java @@ -21,6 +21,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; +import java.util.concurrent.CopyOnWriteArrayList; import org.apache.camel.catalog.CamelCatalog; import org.apache.camel.catalog.DefaultCamelCatalog; @@ -67,6 +68,7 @@ import org.apache.camel.dsl.jbang.core.common.CliException; import org.apache.camel.dsl.jbang.core.common.CliExitCode; import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper; +import org.apache.camel.dsl.jbang.core.common.Plugin; import org.apache.camel.dsl.jbang.core.common.PluginHelper; import org.apache.camel.dsl.jbang.core.common.PluginType; import org.apache.camel.dsl.jbang.core.common.Printer; @@ -90,6 +92,9 @@ public class CamelJBangMain implements Callable<Integer> { private boolean discoverPlugins = true; + // the plugins whose commands were added, to add them to a new command line too + private final List<Plugin> plugins = new CopyOnWriteArrayList<>(); + public static void run(String... args) { run(new CamelJBangMain(), args); } @@ -107,7 +112,51 @@ public class CamelJBangMain implements Callable<Integer> { // ignore } - commandLine = new CommandLine(this) + commandLine = createCommandLine(); + + postAddCommands(commandLine, args); + + if (discoverPlugins && PluginHelper.shouldDiscoverPlugins(commandLine, args)) { + try { + PluginHelper.addPlugins(commandLine, this, args); + PluginHelper.installOnFirstUse(commandLine, this, args); + } catch (CliException e) { + // a plugin that cannot be loaded + out.printErr(e.getMessage()); + quit(e.getExitCode()); + return; + } + } + withErrorHandling(commandLine); + + commandLine.getCommandSpec().versionProvider(() -> { + CamelCatalog catalog = new DefaultCamelCatalog(); + String v = catalog.getCatalogVersion(); + return new String[] { v }; + }); + + CommandLineHelper.migrateLegacyUserConfig(out); + CommandLineHelper.augmentWithUserConfiguration(commandLine); + preExecute(commandLine, args); + int exitCode; + try { + exitCode = commandLine.execute(args); + } catch (Error e) { + // picocli hands exceptions to the handler, but an error (such as a class missing at runtime) escapes it + exitCode = new CliExecutionExceptionHandler(this).handle(e, commandLine.getParseResult()); + } + if (isHelpRequest(args) && EnvironmentHelper.isInteractiveTerminal()) { + printAvailablePlugins(); + } + postExecute(commandLine, args, exitCode); + quit(exitCode); + } + + /** + * The command line with the commands of the CLI. + */ + private CommandLine createCommandLine() { + CommandLine commandLine = new CommandLine(this) .addSubcommand("bind", new CommandLine(new Bind(this))) .addSubcommand("catalog", new CommandLine(new CatalogCommand(this)) .addSubcommand("component", new CommandLine(new CatalogComponent(this))) @@ -256,43 +305,34 @@ public class CamelJBangMain implements Callable<Integer> { commandLine.getHelpSectionMap().put( CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST, new GroupedCommandHelpRenderer()); + return commandLine; + } - postAddCommands(commandLine, args); - - if (discoverPlugins && PluginHelper.shouldDiscoverPlugins(commandLine, args)) { - try { - PluginHelper.addPlugins(commandLine, this, args); - PluginHelper.installOnFirstUse(commandLine, this, args); - } catch (CliException e) { - // a plugin that cannot be loaded - out.printErr(e.getMessage()); - quit(e.getExitCode()); - return; - } + /** + * A new command line with new instances of the commands of the CLI, and of the commands of the plugins this main + * has loaded. A command that runs on it starts from its defaults, not from what a run before left in its fields: + * {@link CliRunner} runs each command on a new command line. + */ + public CommandLine newCommandLine() { + CommandLine answer = createCommandLine(); + postAddCommands(answer, new String[0]); + for (Plugin plugin : plugins) { + plugin.customize(answer, this); } - withErrorHandling(commandLine); - - commandLine.getCommandSpec().versionProvider(() -> { - CamelCatalog catalog = new DefaultCamelCatalog(); - String v = catalog.getCatalogVersion(); - return new String[] { v }; - }); + withErrorHandling(answer); + answer.getCommandSpec().versionProvider(() -> new String[] { new DefaultCamelCatalog().getCatalogVersion() }); + CommandLineHelper.augmentWithUserConfiguration(answer); + return answer; + } - CommandLineHelper.migrateLegacyUserConfig(out); - CommandLineHelper.augmentWithUserConfiguration(commandLine); - preExecute(commandLine, args); - int exitCode; - try { - exitCode = commandLine.execute(args); - } catch (Error e) { - // picocli hands exceptions to the handler, but an error (such as a class missing at runtime) escapes it - exitCode = new CliExecutionExceptionHandler(this).handle(e, commandLine.getParseResult()); - } - if (isHelpRequest(args) && EnvironmentHelper.isInteractiveTerminal()) { - printAvailablePlugins(); + /** + * Remembers a plugin whose commands were added to the command line, to add them to a new command line too (see + * {@link #newCommandLine()}). + */ + public void addPlugin(Plugin plugin) { + if (!plugins.contains(plugin)) { + plugins.add(plugin); } - postExecute(commandLine, args, exitCode); - quit(exitCode); } /** diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelShellCommandRegistry.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelShellCommandRegistry.java index 25c471865985..7e767214844e 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelShellCommandRegistry.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelShellCommandRegistry.java @@ -22,6 +22,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import org.apache.camel.dsl.jbang.core.common.RuntimeUtil; import org.jline.picocli.PicocliCommandRegistry; import org.jline.reader.Completer; import org.jline.shell.Command; @@ -121,6 +122,7 @@ public class CamelShellCommandRegistry extends PicocliCommandRegistry { @Override public Object execute(org.jline.shell.CommandSession session, String[] args) throws Exception { synchronized (CliRunner.LOCK) { + RuntimeUtil.newCommand(); return command.execute(session, args); } } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliRunner.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliRunner.java index 7adcaf05e52a..8603e8cc1e9d 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliRunner.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliRunner.java @@ -27,6 +27,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.apache.camel.dsl.jbang.core.common.CliExitCode; import org.apache.camel.dsl.jbang.core.common.Printer; +import org.apache.camel.dsl.jbang.core.common.RuntimeUtil; import org.apache.camel.dsl.jbang.core.common.StringPrinter; import org.apache.camel.util.json.JsonObject; import picocli.CommandLine; @@ -103,10 +104,12 @@ public final class CliRunner { * @param timeoutMillis how long the command may run, or 0 to wait until it has finished */ public static CliResult run(List<String> args, long timeoutMillis) { - CommandLine commandLine = CamelJBangMain.getCommandLine(); - if (commandLine == null) { + CommandLine main = CamelJBangMain.getCommandLine(); + if (main == null) { throw new IllegalStateException("The Camel CLI is not initialized"); } + // new instances of the commands, so that no field keeps the value a run before left in it + CommandLine commandLine = ((CamelJBangMain) main.getCommand()).newCommandLine(); Output output = new Output(); long start = System.nanoTime(); if (timeoutMillis <= 0) { @@ -175,6 +178,7 @@ public final class CliRunner { commandLine.setOut(output.helpOut); commandLine.setErr(output.helpErr); try { + RuntimeUtil.newCommand(); return commandLine.execute(args.toArray(String[]::new)); } finally { output.helpOut.flush(); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java index 20df3f945ba4..73b96f15471b 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java @@ -115,6 +115,15 @@ public abstract class CatalogBaseCommand extends CamelCommand { } CamelCatalog loadCatalog() throws Exception { + return loadCatalog(runtime, camelVersion, quarkusPlatform, mavenResolver); + } + + /** + * The catalog of the runtime and Camel version: of Spring Boot, of the Quarkus platform, or of Camel. + */ + static CamelCatalog loadCatalog( + RuntimeType runtime, String camelVersion, QuarkusPlatformMixin quarkusPlatform, MavenResolverMixin mavenResolver) + throws Exception { if (RuntimeType.springBoot == runtime) { return CatalogLoader.loadSpringBootCatalog(mavenResolver.repos(), camelVersion, mavenResolver.download()); } else if (RuntimeType.quarkus == runtime) { diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java index 4149fcd2aec8..5de4bbdd0b93 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java @@ -30,14 +30,12 @@ import com.github.freva.asciitable.Column; import com.github.freva.asciitable.HorizontalAlign; import com.github.freva.asciitable.OverflowBehaviour; import org.apache.camel.catalog.CamelCatalog; -import org.apache.camel.catalog.DefaultCamelCatalog; import org.apache.camel.dsl.jbang.core.commands.CamelCommand; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; import org.apache.camel.dsl.jbang.core.commands.MavenResolverMixin; import org.apache.camel.dsl.jbang.core.commands.QuarkusPlatformMixin; import org.apache.camel.dsl.jbang.core.commands.ai.CatalogDocs; import org.apache.camel.dsl.jbang.core.commands.ai.KameletDefinitions; -import org.apache.camel.dsl.jbang.core.common.CatalogLoader; import org.apache.camel.dsl.jbang.core.common.CliException; import org.apache.camel.dsl.jbang.core.common.JsonOutput; import org.apache.camel.dsl.jbang.core.common.RuntimeCompletionCandidates; @@ -129,24 +127,7 @@ public class CatalogDoc extends CamelCommand { } CamelCatalog loadCatalog() throws Exception { - if (RuntimeType.springBoot == runtime) { - return CatalogLoader.loadSpringBootCatalog(mavenResolver.repos(), camelVersion, mavenResolver.download()); - } else if (RuntimeType.quarkus == runtime) { - final MavenGav quarkusCamelBom - = quarkusPlatform - .resolve( - camelVersion, - mavenResolver.downloader()::resolveArtifact, - mavenResolver.download(), - mavenResolver.fresh()) - .quarkusCamelBom(); - return CatalogLoader.loadQuarkusCatalog(quarkusCamelBom, mavenResolver.downloader()::resolveArtifact); - } - if (camelVersion == null) { - return new DefaultCamelCatalog(true); - } else { - return CatalogLoader.loadCatalog(mavenResolver.repos(), camelVersion, mavenResolver.download()); - } + return CatalogBaseCommand.loadCatalog(runtime, camelVersion, quarkusPlatform, mavenResolver); } @Override diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java index 6cc8c474c0a1..4bd5a3349c94 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java @@ -154,7 +154,7 @@ public final class PluginHelper { continue; } - plugin.getValue().customize(commandLine, main); + apply(plugin.getValue(), commandLine, main); } } @@ -180,12 +180,18 @@ public final class PluginHelper { main.getOut().printDiagnostic("Installed plugin: " + pluginType.get().getName()); for (Map.Entry<String, Plugin> plugin : getActivePlugins(main, resolveRepos(args), target).entrySet()) { if (!commandLine.getSubcommands().containsKey(plugin.getKey())) { - plugin.getValue().customize(commandLine, main); + apply(plugin.getValue(), commandLine, main); } } return true; } + // adds the commands of the plugin, and the main remembers the plugin for the command lines it makes later + private static void apply(Plugin plugin, CommandLine commandLine, CamelJBangMain main) { + plugin.customize(commandLine, main); + main.addPlugin(plugin); + } + // allow to download plugins from 3rd party maven repositories by --repos argument, or the user configuration private static String resolveRepos(String... args) { String repos = null; @@ -800,7 +806,7 @@ public final class PluginHelper { return true; } - plugin.customize(commandLine, main); + apply(plugin, commandLine, main); return true; } } catch (Exception e) { diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeUtil.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeUtil.java index b2c8fbe3b25e..1737f18ef6e1 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeUtil.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeUtil.java @@ -20,11 +20,13 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; -import java.util.concurrent.atomic.AtomicBoolean; import org.apache.camel.util.IOHelper; import org.apache.camel.util.OrderedProperties; @@ -34,7 +36,10 @@ import org.apache.logging.log4j.core.config.Configurator; public final class RuntimeUtil { - private static final AtomicBoolean INIT_DONE = new AtomicBoolean(); + // the logging configuration log4j was configured with, or null when not yet + private static String configured; + // whether the command that runs next may configure the logging again (see newCommand) + private static boolean newCommand; private RuntimeUtil() { } @@ -50,84 +55,119 @@ public final class RuntimeUtil { String level, boolean color, boolean json, boolean script, boolean export, String loggingConfigPath, List<String> loggingCategories, boolean mcpStdio) throws Exception { - if (INIT_DONE.compareAndSet(false, true)) { - long pid = ProcessHandle.current().pid(); - System.setProperty("pid", Long.toString(pid)); - // make it possible to manage log4j via JMX and camel-cli - System.setProperty("log4j2.disable.jmx", "false"); - - if (loggingConfigPath != null) { - // ust custom logging configuration as-is - Configurator.initialize("CamelJBang", "file://" + Path.of(loggingConfigPath).toAbsolutePath()); - } else if (mcpStdio) { - Configurator.initialize("CamelJBang", "log4j2-mcp-stdio.properties"); - } else if (loggingCategories != null && !loggingCategories.isEmpty()) { - // enrich logging file with custom logging categories - String name = "log4j2-no-color.properties"; - if (export) { - name = "log4j2-export.properties"; - } else if (script) { - name = "log4j2-script.properties"; - } else if (json) { - name = "log4j2-json.properties"; - } else if (color) { - name = "log4j2.properties"; - } - InputStream is = RuntimeUtil.class.getClassLoader().getResourceAsStream(name); - String content = IOHelper.loadText(is); - IOHelper.close(is); - - StringJoiner sj = new StringJoiner(System.lineSeparator()); - int i = 0; - for (String lc : loggingCategories) { - String prefix = "custom" + i++; - String catName = StringHelper.before(lc, "=", "").trim(); - String catLevel = StringHelper.after(lc, "=", "").trim(); - if ("root".equalsIgnoreCase(catName)) { - // log4j allows one root logger only, which the template already defines: a root category is - // the root logging level - if (!catLevel.isEmpty()) { - level = catLevel; - } - continue; - } - if (!catName.isEmpty() && !catLevel.isEmpty()) { - sj.add("logger." + prefix + ".name=" + catName); - sj.add("logger." + prefix + ".level=" + catLevel); - // the category gets the same appenders as the root logger, so it must not also pass its - // events up to the root, or every line is printed twice (CAMEL-24701) - sj.add("logger." + prefix + ".additivity=false"); - if (!export && !script) { - sj.add("logger." + prefix + ".appenderRef.$1.ref=out"); - } - sj.add("logger." + prefix + ".appenderRef.$2.ref=file"); + String location; + if (loggingConfigPath != null) { + // ust custom logging configuration as-is + location = Path.of(loggingConfigPath).toAbsolutePath().toUri().toString(); + } else if (mcpStdio) { + location = "log4j2-mcp-stdio.properties"; + } else if (loggingCategories != null && !loggingCategories.isEmpty()) { + // enrich logging file with custom logging categories + String name = "log4j2-no-color.properties"; + if (export) { + name = "log4j2-export.properties"; + } else if (script) { + name = "log4j2-script.properties"; + } else if (json) { + name = "log4j2-json.properties"; + } else if (color) { + name = "log4j2.properties"; + } + InputStream is = RuntimeUtil.class.getClassLoader().getResourceAsStream(name); + String content = IOHelper.loadText(is); + IOHelper.close(is); + + StringJoiner sj = new StringJoiner(System.lineSeparator()); + int i = 0; + for (String lc : loggingCategories) { + String prefix = "custom" + i++; + String catName = StringHelper.before(lc, "=", "").trim(); + String catLevel = StringHelper.after(lc, "=", "").trim(); + if ("root".equalsIgnoreCase(catName)) { + // log4j allows one root logger only, which the template already defines: a root category is + // the root logging level + if (!catLevel.isEmpty()) { + level = catLevel; } + continue; } - content = content + System.lineSeparator() + sj; - - name = CommandLineHelper.CAMEL_JBANG_WORK_DIR + "/log4j2.properties"; - Files.writeString(Paths.get(name), content); - - Configurator.initialize("CamelJBang", Path.of(name).toAbsolutePath().toUri().toString()); - } else { - // use out of the box logging configuration - if (export) { - Configurator.initialize("CamelJBang", "log4j2-export.properties"); - } else if (script) { - Configurator.initialize("CamelJBang", "log4j2-script.properties"); - } else if (json) { - Configurator.initialize("CamelJBang", "log4j2-json.properties"); - } else if (color) { - Configurator.initialize("CamelJBang", "log4j2.properties"); - } else { - Configurator.initialize("CamelJBang", "log4j2-no-color.properties"); + if (!catName.isEmpty() && !catLevel.isEmpty()) { + sj.add("logger." + prefix + ".name=" + catName); + sj.add("logger." + prefix + ".level=" + catLevel); + // the category gets the same appenders as the root logger, so it must not also pass its + // events up to the root, or every line is printed twice (CAMEL-24701) + sj.add("logger." + prefix + ".additivity=false"); + if (!export && !script) { + sj.add("logger." + prefix + ".appenderRef.$1.ref=out"); + } + sj.add("logger." + prefix + ".appenderRef.$2.ref=file"); } } + content = content + System.lineSeparator() + sj; + + name = CommandLineHelper.CAMEL_JBANG_WORK_DIR + "/log4j2.properties"; + Files.writeString(Paths.get(name), content); + // the categories are part of what the command asks for + location = Path.of(name).toAbsolutePath().toUri() + "#" + content.hashCode(); + } else if (export) { + // use out of the box logging configuration + location = "log4j2-export.properties"; + } else if (script) { + location = "log4j2-script.properties"; + } else if (json) { + location = "log4j2-json.properties"; + } else if (color) { + location = "log4j2.properties"; + } else { + location = "log4j2-no-color.properties"; + } + + synchronized (RuntimeUtil.class) { + if (configured == null) { + long pid = ProcessHandle.current().pid(); + System.setProperty("pid", Long.toString(pid)); + // make it possible to manage log4j via JMX and camel-cli + System.setProperty("log4j2.disable.jmx", "false"); + Configurator.initialize("CamelJBang", withoutHash(location)); + configured = location; + } else if (newCommand && !configured.equals(location)) { + // a CLI that runs several commands in this JVM (camel ask, camel shell, the monitor) runs one that + // logs otherwise, such as export to its log file: log4j is configured once, so configure it again + Configurator.reconfigure(toUri(withoutHash(location))); + configured = location; + } + // as for a command run on its own, the first configuration of the command is the one it logs with + newCommand = false; } setRootLoggingLevel(level); } + /** + * A CLI that runs several commands in this JVM (camel ask, camel shell, the monitor) runs another one: its first + * logging configuration is applied, as for a command run on its own, also when log4j was configured otherwise by a + * command before. + */ + public static void newCommand() { + synchronized (RuntimeUtil.class) { + newCommand = true; + } + } + + private static String withoutHash(String location) { + int pos = location.lastIndexOf('#'); + return pos > 0 ? location.substring(0, pos) : location; + } + + // a configuration on the classpath, or a file + private static URI toUri(String location) throws URISyntaxException { + if (location.startsWith("file:")) { + return new URI(location); + } + URL url = RuntimeUtil.class.getClassLoader().getResource(location); + return url != null ? url.toURI() : new URI(location); + } + public static void setRootLoggingLevel(String level) { if (level != null) { level = level.toLowerCase(); diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CliRunnerTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CliRunnerTest.java index 0cbd6ebc43fd..583fdee0e177 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CliRunnerTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CliRunnerTest.java @@ -139,6 +139,13 @@ class CliRunnerTest { assertThat(config).doesNotExist(); } + @Test + void everyRunHasNewCommands() { + // a field that is not an option keeps its value only when the same instance runs again + assertThat(CliRunner.run(List.of("count")).stdout()).isEqualTo("runs=1"); + assertThat(CliRunner.run(List.of("count")).stdout()).isEqualTo("runs=1"); + } + @Test void resultAsJson() { JsonObject json = new CliRunner.CliResult(3, "out", "x".repeat(10), 12, false).toJson(5); @@ -153,8 +160,9 @@ class CliRunnerTest { private static class TestMain extends CamelJBangMain { @Override public void postAddCommands(CommandLine commandLine, String[] args) { - // the commands of the CLI, and one that ignores the interrupt + // the commands of the CLI, one that ignores the interrupt, and one that counts its runs commandLine.addSubcommand("busy", new CommandLine(new BusyCommand(this))); + commandLine.addSubcommand("count", new CommandLine(new CountCommand(this))); } @Override @@ -163,6 +171,31 @@ class CliRunnerTest { } } + /** + * A command that counts its runs in a field that is not an option. + */ + @CommandLine.Command(name = "count") + static class CountCommand extends CamelCommand { + + private int runs; + + CountCommand(CamelJBangMain main) { + super(main); + } + + @Override + protected boolean disarrangeLogging() { + return false; + } + + @Override + public Integer doCall() { + runs++; + printer().println("runs=" + runs); + return 0; + } + } + /** * A command that runs for 3 seconds whatever happens, as one blocked in I/O does. */ diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/RuntimeUtilLoggingTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/RuntimeUtilLoggingTest.java new file mode 100644 index 000000000000..f41f7d2cbf93 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/RuntimeUtilLoggingTest.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.common; + +import java.util.HashSet; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LoggerContext; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The logging of a command: configured once, as a command runs on its own, but a CLI that runs several commands in this + * JVM (camel ask, camel shell, the monitor) gives each of them its own. + */ +class RuntimeUtilLoggingTest { + + @Test + void newCommandOfACliThatRunsSeveralCommandsLogsAsOnItsOwn() throws Exception { + RuntimeUtil.configureLog("off", false, false, false, false, null, null); + Set<String> first = appenders(); + + // a command run on its own keeps the configuration it started with + RuntimeUtil.configureLog("info", false, false, false, true, null, null); + assertThat(appenders()).isEqualTo(first); + + // the next command of camel ask or the monitor logs as it would on its own: export to its log file only + RuntimeUtil.newCommand(); + RuntimeUtil.configureLog("info", false, false, false, true, null, null); + assertThat(appenders()).containsExactly("file"); + + // and the one after it logs to the console again + RuntimeUtil.newCommand(); + RuntimeUtil.configureLog("off", false, false, false, false, null, null); + assertThat(appenders()).contains("out"); + } + + private static Set<String> appenders() { + LoggerContext context = (LoggerContext) LogManager.getContext(false); + return new HashSet<>(context.getConfiguration().getAppenders().keySet()); + } +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiCliCommandExecutorTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiCliCommandExecutorTest.java index c59d87960362..cf6d3ad05e1a 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiCliCommandExecutorTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiCliCommandExecutorTest.java @@ -136,10 +136,15 @@ class AiCliCommandExecutorTest { @Test void productionInvokerRestoresPrinterAndDoesNotQuitMain() throws Exception { - CamelJBangMain main = new CamelJBangMain(); + // a command runs on new instances of the commands, so the test command is added the way plugins add theirs + CamelJBangMain main = new CamelJBangMain() { + @Override + public void postAddCommands(CommandLine commandLine, String[] args) { + commandLine.addSubcommand("tui-test", new CommandLine(new TuiTestCommand(this))); + } + }; Printer originalPrinter = main.getOut(); - CommandLine commandLine = new CommandLine(main) - .addSubcommand("tui-test", new CommandLine(new TuiTestCommand(main))); + CommandLine commandLine = new CommandLine(main); Field field = CamelJBangMain.class.getDeclaredField("commandLine"); field.setAccessible(true); CommandLine previous = (CommandLine) field.get(null);
