This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 23457461383d CAMEL-25400: camel-jbang - The CLI that runs several
commands gives each a fresh start (#27962)
23457461383d is described below
commit 23457461383da81f7d1a8337fa5cbffce7e9e8d6
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 16 20:37:36 2026 +0200
CAMEL-25400: camel-jbang - The CLI that runs several commands gives each a
fresh start (#27962)
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]>
---
.../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..11f565b8907b 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) {
+ // use 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);