This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/CAMEL-24699-existing-project-log in repository https://gitbox.apache.org/repos/asf/camel.git
commit 8d3b6f383bc31f432c957fa9bcb5d3e34b2724ec Author: Claus Ibsen <[email protected]> AuthorDate: Wed Sep 16 21:58:51 2026 +0200 CAMEL-24699: camel-jbang - camel run on an existing Quarkus or Camel Main project logs to ~/.camel An existing Maven project run with camel run pom.xml now logs to ~/.camel/<name>.log for all three runtimes, so camel log, the TUI Log tab and the MCP log resource can read it. Quarkus gets the quarkus.log.file.* properties via jvm.args, Camel Main a rendered log4j2 configuration in target/ via -Dlog4j2.configurationFile on the Maven command line (camel:run runs Camel inside the Maven JVM). The name is camel.main.name from application.properties or the artifactId, and is passed to the application so it matches the file the readers look for. The Camel Main path passed --profile, --jfr and --jvm-args via -Dcamel.jvmArgs, which is not a camel-maven-plugin parameter, so they were dropped: the profile is now a plain -D and JVM options go through MAVEN_OPTS. camel:run also does not compile the project, so compile is run first. --port, --prop and the --max-* limits are passed to all three runtimes. The TUI ran Maven projects with its own launcher (only Spring Boot logged to a file, same camel.jvmArgs problem); it now runs them with camel run pom.xml so there is one implementation. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../modules/ROOT/pages/camel-jbang-running.adoc | 3 +- .../modules/ROOT/pages/camel-jbang-tips.adoc | 16 +- .../modules/ROOT/pages/camel-jbang-tui.adoc | 13 +- .../apache/camel/dsl/jbang/core/commands/Run.java | 212 ++++++++++++++------ .../core/commands/RunExistingProjectTest.java | 178 +++++++++++++++++ .../dsl/jbang/core/commands/tui/CamelMonitor.java | 1 - .../dsl/jbang/core/commands/tui/LaunchManager.java | 214 ++------------------- 7 files changed, 369 insertions(+), 268 deletions(-) diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc index cefdef8abcff..0248dab74ba7 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc @@ -280,7 +280,8 @@ camel run foo.camel.yaml --runtime=quarkus --quarkus-version=3.9.4 ---- When running an existing Maven project (`camel run pom.xml`) the runtime is detected from the -`pom.xml`, as such a project cannot run in-process. +`pom.xml`, as such a project cannot run in-process. The application logs to `~/.camel/<name>.log` +so `camel log` and the TUI can read the logs; see xref:camel-jbang-tips.adoc#_running_a_maven_based_project[Running a Maven based project]. == Running local Kamelets diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tips.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-tips.adoc index 3553b63d3195..9245598ca7a4 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tips.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tips.adoc @@ -210,14 +210,22 @@ See the https://maven.apache.org/guides/mini/guide-encryption.html[Maven encrypt == Running a Maven based project -Camel CLI can do a _best effort_ run of an existing Maven-based project for migration purposes: +Camel CLI can run an existing Maven-based project: [source,bash] ---- camel run pom.xml ---- -Camel CLI scans `src/main/java` and `src/main/resources` for files to include. +The runtime is detected from the `pom.xml` (Spring Boot, Quarkus, or Camel Main), and the project is +built and run with Maven (`spring-boot:run`, `quarkus:dev` with `--dev` or `quarkus:run`, and `camel:run`). +The project itself is not modified: the `camel-cli-connector` dependency is injected into a temporary +`camel-jbang-run-pom.xml`, so the application shows up in `camel ps`, and the application logs to +`~/.camel/<name>.log` (`<name>` is `camel.main.name` from the project's `application.properties`, or else +the Maven artifactId) so `camel log`, the TUI and the MCP server can read the logs the same as for any +other `camel run`. The temporary files are removed when the application stops. -NOTE: This is not a fully compatible way to run Maven projects. It cannot start Spring Boot or Quarkus -applications — use the proper plugins for those. This command is mainly useful for migrating from old projects. +The options `--profile`, `--port`, `--prop`, `--max-seconds`, `--max-messages`, `--max-idle-seconds`, +`--jvm-args` and `--jfr` are passed to the application (Camel Main runs inside the Maven JVM, so +`--jvm-args` and `--jfr` are set via `MAVEN_OPTS`). A Camel Main project is expected to use log4j2 +for logging (the same as `camel export --runtime=main` generates). diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc index c44e615e4382..3b601f254a46 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc @@ -81,8 +81,10 @@ camel tui /path/to/my-project The TUI opens the directory in the Source tab so you can browse the project files immediately. When a `pom.xml` is present, the runtime is auto-detected (Spring Boot, Quarkus, or Camel Main). -Press *F10* to run the project -- Maven projects are launched with the appropriate goal -(`spring-boot:run`, `quarkus:dev`, or `camel:run`), and plain directories are run with `camel run`. +Press *F10* to run the project -- Maven projects are run with `camel run pom.xml`, which launches +them with the appropriate goal (`spring-boot:run`, `quarkus:dev`, or `camel:run`), and plain +directories are run with `camel run`. Either way the application logs to `~/.camel` so the Log tab +shows its logs. This is a quick way to explore and run any Camel project without starting it separately first. @@ -117,10 +119,9 @@ Once added, start your application normally and the TUI will discover it automat No additional configuration is needed -- the connector auto-detects on the classpath and registers the application with the local Camel CLI. -TIP: When you open a Spring Boot project via *F2 > Open Project* and run it with *F10*, -the TUI automatically injects the `camel-cli-connector-starter` dependency if it's not -already in your `pom.xml`. This means the TUI can monitor the application without -modifying your project. +TIP: When you open a Spring Boot, Quarkus or Camel Main project via *F2 > Open Project* and run +it with *F10*, the CLI connector dependency is automatically injected if it's not already in your +`pom.xml`. This means the TUI can monitor the application without modifying your project. See xref:camel-jbang-managing.adoc[Managing Integrations] for more details. diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java index dc81b39f169d..3a69b153d406 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java @@ -149,6 +149,11 @@ public class Run extends CamelCommand { private static final String OPENAPI_GENERATED_FILE = CommandLineHelper.CAMEL_JBANG_WORK_DIR + "/generated-openapi.yaml"; private static final String CLIPBOARD_GENERATED_FILE = CommandLineHelper.CAMEL_JBANG_WORK_DIR + "/generated-clipboard"; + // the same layout as the camel-jbang console logging, so the log file reads the same for every runtime + static final String QUARKUS_LOG_FILE_FORMAT + = "%d{yyyy-MM-dd HH:mm:ss.SSS} %5p %i --- [%15.15t] %-40.40c{3.} : %s%e%n"; + // the file with the log4j2 configuration that an existing Camel Main project is run with + static final String CAMEL_MAIN_RUN_LOG_CONFIG = "camel-jbang-run-log4j2.properties"; private static final Pattern PACKAGE_PATTERN = Pattern.compile( "^\\s*package\\s+([a-zA-Z][.\\w]*)\\s*;.*$", Pattern.MULTILINE); @@ -1572,7 +1577,7 @@ public class Run extends CamelCommand { content += "\n# logging to file\n" + "quarkus.log.file.enabled=true\n" + "quarkus.log.file.path=${user.home}/.camel/" + eq.name + ".log\n" - + "quarkus.log.file.format=%d{yyyy-MM-dd HH:mm:ss.SSS} %5p %i --- [%15.15t] %-40.40c{3.} : %s%e%n\n"; + + "quarkus.log.file.format=" + QUARKUS_LOG_FILE_FORMAT + "\n"; Files.writeString(appProps, content); } @@ -1832,19 +1837,90 @@ public class Run extends CamelCommand { return fallback; } + /** + * The application name of an existing Maven project: {@code camel.main.name} from its application.properties, or + * else the Maven artifactId (or the project directory name). The name is also passed to the application as + * {@code camel.main.name}, so the name it reports to the CLI and the TUI matches the {@code <name>.log} file that + * {@code camel log} and the TUI read when there is no {@code <pid>.log}. + */ + static String resolveExistingProjectAppName(Path projectDir, Model model) { + String name = resolveExportedAppName(projectDir, null); + if (name == null && model != null && model.getArtifactId() != null && !model.getArtifactId().isBlank()) { + name = model.getArtifactId().trim(); + } + if (name == null) { + name = projectDir.getFileName().toString(); + } + return name; + } + + /** + * The {@code -D} system properties an existing Maven project is run with: the HTTP port ({@code --port}, as the + * given property), the execution limits ({@code --max-seconds}, {@code --max-messages} and + * {@code --max-idle-seconds}) and the additional properties ({@code --prop}). + */ + List<String> buildExistingProjectSystemProperties(String portKey) { + List<String> args = new ArrayList<>(); + if (serverOptions.port != -1) { + args.add("-D" + portKey + "=" + serverOptions.port); + } + if (executionLimitOptions.maxSeconds > 0) { + args.add("-Dcamel.main.durationMaxSeconds=" + executionLimitOptions.maxSeconds); + } + if (executionLimitOptions.maxMessages > 0) { + args.add("-Dcamel.main.durationMaxMessages=" + executionLimitOptions.maxMessages); + } + if (executionLimitOptions.maxIdleSeconds > 0) { + args.add("-Dcamel.main.durationMaxIdleSeconds=" + executionLimitOptions.maxIdleSeconds); + } + if (property != null) { + for (String p : property) { + String s = p.trim(); + if (!s.isEmpty()) { + args.add(s.startsWith("-D") ? s : "-D" + s); + } + } + } + return args; + } + + /** + * The Quarkus logging properties that make an existing Quarkus project also log to the {@code <name>.log} file in + * the {@code ~/.camel} directory (the same as the exported project does via its application.properties). + */ + static List<String> buildQuarkusLogFileJvmArgs(String appName) { + Path logFile = CommandLineHelper.getCamelDir().resolve(appName + ".log"); + String fileName = logFile.toAbsolutePath().toString().replace("\\", "/"); + List<String> args = new ArrayList<>(); + args.add("-Dquarkus.log.file.enabled=true"); + args.add("-Dquarkus.log.file.path=" + fileName); + // quoted as the format has spaces (quarkus:dev parses jvm.args as a command line) + args.add("-Dquarkus.log.file.format=\"" + QUARKUS_LOG_FILE_FORMAT + "\""); + return args; + } + /** * Writes a log4j2 configuration to the exported Camel Main project that logs to the console, and to the * {@code <name>.log} file in the {@code ~/.camel} directory. */ private void writeCamelMainRunLogConfig(Path runDirPath, String appName) throws IOException { + writeCamelMainRunLogConfig(runDirPath.resolve("src/main/resources/log4j2.properties"), appName, + loggingOptions.loggingLevel); + } + + /** + * Writes the log4j2 configuration used by {@code camel run --runtime=main} to the given file. It logs to the + * console, and to the {@code <name>.log} file in the {@code ~/.camel} directory (which is deleted first, so it does + * not start with the output of a previous run). + */ + static void writeCamelMainRunLogConfig(Path target, String appName, String loggingLevel) throws IOException { Path logFile = CommandLineHelper.getCamelDir().resolve(appName + ".log"); Files.deleteIfExists(logFile); String fileName = logFile.toAbsolutePath().toString().replace("\\", "/"); try (InputStream is = Run.class.getClassLoader().getResourceAsStream("camel-main-run-log4j2.properties")) { String content = new String(is.readAllBytes(), StandardCharsets.UTF_8) .replace("{{logFile}}", fileName) - .replace("{{level}}", loggingOptions.loggingLevel != null ? loggingOptions.loggingLevel : "info"); - Path target = runDirPath.resolve("src/main/resources/log4j2.properties"); + .replace("{{level}}", loggingLevel != null ? loggingLevel : "info"); Files.createDirectories(target.getParent()); Files.writeString(target, content); } @@ -1937,6 +2013,11 @@ public class Run extends CamelCommand { w.write(fos, model); } + // log to console and to file in ~/.camel (so camel log and the TUI can tail the logs) + String appName = resolveExistingProjectAppName(projectDir, model); + Path logConfig = projectDir.resolve("target").resolve(CAMEL_MAIN_RUN_LOG_CONFIG); + writeCamelMainRunLogConfig(logConfig, appName, loggingOptions.loggingLevel); + // shutdown hook to clean up temp files Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { @@ -1955,6 +2036,7 @@ public class Run extends CamelCommand { } } Files.deleteIfExists(tempPom); + Files.deleteIfExists(logConfig); } catch (Exception e) { // ignore } @@ -1970,27 +2052,11 @@ public class Run extends CamelCommand { cmd.add("--quiet"); cmd.add("--file"); cmd.add(tempPom.toString()); - StringBuilder camelJvmArgs = new StringBuilder(); - if (profile != null && !"prod".equals(profile)) { - camelJvmArgs.append("-Dcamel.main.profile=").append(profile); - } - String camelJfrArg = buildJfrJvmArgs(); - if (camelJfrArg != null) { - if (!camelJvmArgs.isEmpty()) { - camelJvmArgs.append(" "); - } - camelJvmArgs.append(camelJfrArg); - } - if (jvmArgs != null && !jvmArgs.isBlank()) { - if (!camelJvmArgs.isEmpty()) { - camelJvmArgs.append(" "); - } - camelJvmArgs.append(jvmArgs.trim()); - } - if (!camelJvmArgs.isEmpty()) { - cmd.add("-Dcamel.jvmArgs=" + camelJvmArgs); - } + // camel:run runs Camel inside the Maven JVM, so the system properties go directly on the Maven command line + cmd.addAll(buildExistingCamelMainSystemProperties(appName, logConfig)); cmd.add("-DskipTests"); + // camel:run does not compile the project itself + cmd.add("compile"); cmd.add("camel:run"); printer().println("Running Camel Main project: " + projectDir); @@ -1999,12 +2065,32 @@ public class Run extends CamelCommand { pb.command(cmd); pb.directory(projectDir.toFile()); pb.inheritIO(); + // JVM options (--jvm-args and --jfr) must be set on the Maven JVM as Camel runs inside it + String mavenOpts = mergeJvmArgs(jvmArgs, buildJfrJvmArgs()); + if (mavenOpts != null && !mavenOpts.isBlank()) { + pb.environment().merge("MAVEN_OPTS", mavenOpts.trim(), (existing, extra) -> existing + " " + extra); + } Process p = pb.start(); processRef.set(p); this.spawnPid = p.pid(); return p.waitFor(); } + /** + * The {@code -D} system properties an existing Camel Main project is run with (Camel runs inside the Maven JVM so + * they go on the Maven command line): the log4j2 configuration, the profile, the name, the port and properties. + */ + List<String> buildExistingCamelMainSystemProperties(String appName, Path logConfig) { + List<String> args = new ArrayList<>(); + args.add("-Dlog4j2.configurationFile=" + logConfig.toAbsolutePath()); + if (profile != null && !"prod".equals(profile)) { + args.add("-Dcamel.main.profile=" + profile); + } + args.add("-Dcamel.main.name=" + appName); + args.addAll(buildExistingProjectSystemProperties("camel.server.port")); + return args; + } + private int runExistingQuarkusProject(AtomicReference<Process> processRef) throws Exception { Path pomPath = Path.of(files.get(0)).toAbsolutePath(); Path projectDir = pomPath.getParent(); @@ -2070,31 +2156,16 @@ public class Run extends CamelCommand { Path mvnwPath = projectDir.resolve(mvnw); String mvnCmd = Files.isExecutable(mvnwPath) ? mvnwPath.toString() : "mvn"; + // log to console and to file in ~/.camel (so camel log and the TUI can tail the logs) + String appName = resolveExistingProjectAppName(projectDir, model); + Files.deleteIfExists(CommandLineHelper.getCamelDir().resolve(appName + ".log")); + List<String> cmd = new ArrayList<>(); cmd.add(mvnCmd); cmd.add("--quiet"); cmd.add("--file"); cmd.add(tempPom.toString()); - StringBuilder quarkusJvmArgs = new StringBuilder(); - if (profile != null && !"prod".equals(profile)) { - quarkusJvmArgs.append("-Dcamel.main.profile=").append(profile); - } - String quarkusJfrArg = buildJfrJvmArgs(); - if (quarkusJfrArg != null) { - if (!quarkusJvmArgs.isEmpty()) { - quarkusJvmArgs.append(" "); - } - quarkusJvmArgs.append(quarkusJfrArg); - } - if (jvmArgs != null && !jvmArgs.isBlank()) { - if (!quarkusJvmArgs.isEmpty()) { - quarkusJvmArgs.append(" "); - } - quarkusJvmArgs.append(jvmArgs.trim()); - } - if (!quarkusJvmArgs.isEmpty()) { - cmd.add("-Djvm.args=" + quarkusJvmArgs); - } + cmd.add("-Djvm.args=" + String.join(" ", buildExistingQuarkusJvmArgs(appName))); cmd.add("-DskipTests"); cmd.add("package"); cmd.add("quarkus:" + (dev ? "dev" : "run")); @@ -2111,6 +2182,27 @@ public class Run extends CamelCommand { return p.waitFor(); } + /** + * The JVM arguments ({@code jvm.args}) an existing Quarkus project is run with: logging to file, the profile, the + * name, the port and properties, the flight recording and {@code --jvm-args}. + */ + List<String> buildExistingQuarkusJvmArgs(String appName) { + List<String> args = new ArrayList<>(buildQuarkusLogFileJvmArgs(appName)); + if (profile != null && !"prod".equals(profile)) { + args.add("-Dcamel.main.profile=" + profile); + } + args.add("-Dcamel.main.name=" + appName); + args.addAll(buildExistingProjectSystemProperties("quarkus.http.port")); + String jfrArg = buildJfrJvmArgs(); + if (jfrArg != null) { + args.add(jfrArg); + } + if (jvmArgs != null && !jvmArgs.isBlank()) { + args.add(jvmArgs.trim()); + } + return args; + } + protected int runSpringBoot() throws Exception { if (background) { printer().printErr("Run Camel Spring Boot with --background is not supported"); @@ -2251,6 +2343,27 @@ public class Run extends CamelCommand { return p.waitFor(); } + /** + * The JVM arguments ({@code spring-boot.run.jvmArguments}) an existing Spring Boot project is run with: logging to + * file, the profile, the port and properties, the flight recording and {@code --jvm-args}. + */ + List<String> buildExistingSpringBootJvmArgs() { + List<String> args = new ArrayList<>(); + args.add("-Dlogging.config=classpath:logback-camel-jbang.xml"); + if (profile != null && !"prod".equals(profile)) { + args.add("-Dcamel.main.profile=" + profile); + } + args.addAll(buildExistingProjectSystemProperties("server.port")); + String jfrArg = buildJfrJvmArgs(); + if (jfrArg != null) { + args.add(jfrArg); + } + if (jvmArgs != null && !jvmArgs.isBlank()) { + args.add(jvmArgs.trim()); + } + return args; + } + private int runExistingSpringBootProject(AtomicReference<Process> processRef) throws Exception { Path pomPath = Path.of(files.get(0)).toAbsolutePath(); Path projectDir = pomPath.getParent(); @@ -2330,18 +2443,7 @@ public class Run extends CamelCommand { cmd.add("--quiet"); cmd.add("--file"); cmd.add(tempPom.toString()); - StringBuilder sbJvmArgs = new StringBuilder("-Dlogging.config=classpath:logback-camel-jbang.xml"); - if (profile != null && !"prod".equals(profile)) { - sbJvmArgs.append(" -Dcamel.main.profile=").append(profile); - } - String sbJfrArg = buildJfrJvmArgs(); - if (sbJfrArg != null) { - sbJvmArgs.append(" ").append(sbJfrArg); - } - if (jvmArgs != null && !jvmArgs.isBlank()) { - sbJvmArgs.append(" ").append(jvmArgs.trim()); - } - cmd.add("-Dspring-boot.run.jvmArguments=" + sbJvmArgs); + cmd.add("-Dspring-boot.run.jvmArguments=" + String.join(" ", buildExistingSpringBootJvmArgs())); cmd.add("-DskipTests"); cmd.add("package"); cmd.add("spring-boot:run"); diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunExistingProjectTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunExistingProjectTest.java new file mode 100644 index 000000000000..d40c744043f2 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunExistingProjectTest.java @@ -0,0 +1,178 @@ +/* + * 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.commands; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; +import org.apache.maven.model.Model; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.api.parallel.Isolated; +import picocli.CommandLine; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies how {@code camel run pom.xml} runs an existing Maven project: the application logs to a file in + * {@code ~/.camel} that {@code camel log} and the TUI can read, and the options are passed to the right JVM for each + * runtime. + */ +@Isolated +class RunExistingProjectTest extends CamelCommandBaseTestSupport { + + @TempDir + Path home; + + private String originalHome; + + @BeforeEach + void useTempHome() { + originalHome = CommandLineHelper.getHomeDir().toString(); + CommandLineHelper.useHomeDir(home.toString()); + } + + @AfterEach + void restoreHome() { + CommandLineHelper.useHomeDir(originalHome); + } + + private static Run run(String... args) { + Run command = new Run(new CamelJBangMain()); + CommandLine.populateCommand(command, args); + return command; + } + + private static Model model(String artifactId) { + Model model = new Model(); + model.setArtifactId(artifactId); + return model; + } + + @Test + void appNameIsCamelMainNameFromApplicationProperties(@TempDir Path project) throws Exception { + Path props = project.resolve("src/main/resources/application.properties"); + Files.createDirectories(props.getParent()); + Files.writeString(props, "camel.main.name = my-app\n"); + + assertThat(Run.resolveExistingProjectAppName(project, model("my-artifact"))).isEqualTo("my-app"); + } + + @Test + void appNameFallsBackToArtifactIdThenDirectoryName(@TempDir Path project) { + assertThat(Run.resolveExistingProjectAppName(project, model("my-artifact"))).isEqualTo("my-artifact"); + assertThat(Run.resolveExistingProjectAppName(project, new Model())) + .isEqualTo(project.getFileName().toString()); + assertThat(Run.resolveExistingProjectAppName(project, null)).isEqualTo(project.getFileName().toString()); + } + + @Test + void camelMainLogsToFileViaLog4j2ConfigurationOnTheMavenCommandLine(@TempDir Path project) throws Exception { + Path logConfig = project.resolve("target").resolve(Run.CAMEL_MAIN_RUN_LOG_CONFIG); + Path logFile = CommandLineHelper.getCamelDir().resolve("my-app.log"); + Files.createDirectories(logFile.getParent()); + Files.writeString(logFile, "stale output of a previous run"); + + Run.writeCamelMainRunLogConfig(logConfig, "my-app", "debug"); + + // the previous log is removed so the readers do not show stale lines + assertThat(logFile).doesNotExist(); + String content = Files.readString(logConfig); + assertThat(content).contains("appender.file.fileName = " + logFile.toAbsolutePath().toString().replace("\\", "/")); + assertThat(content).contains("rootLogger.level = debug"); + assertThat(content).doesNotContain("{{"); + + List<String> args = run("--profile=dev", "--port=9090", "--max-seconds=30", "--max-messages=5", + "--max-idle-seconds=10", "--prop=foo=bar", "--prop=-Dbar=baz", "pom.xml") + .buildExistingCamelMainSystemProperties("my-app", logConfig); + assertThat(args).containsExactly( + "-Dlog4j2.configurationFile=" + logConfig.toAbsolutePath(), + "-Dcamel.main.profile=dev", + "-Dcamel.main.name=my-app", + "-Dcamel.server.port=9090", + "-Dcamel.main.durationMaxSeconds=30", + "-Dcamel.main.durationMaxMessages=5", + "-Dcamel.main.durationMaxIdleSeconds=10", + "-Dfoo=bar", + "-Dbar=baz"); + } + + @Test + void camelMainDefaultsToInfoLevelAndNoProfileOrPort(@TempDir Path project) throws Exception { + Path logConfig = project.resolve("target").resolve(Run.CAMEL_MAIN_RUN_LOG_CONFIG); + Run.writeCamelMainRunLogConfig(logConfig, "my-app", null); + assertThat(Files.readString(logConfig)).contains("rootLogger.level = info"); + + // the default profile (dev) is passed, prod is not as it is the default of a Maven project + List<String> args = run("pom.xml").buildExistingCamelMainSystemProperties("my-app", logConfig); + assertThat(args).containsExactly( + "-Dlog4j2.configurationFile=" + logConfig.toAbsolutePath(), + "-Dcamel.main.profile=dev", + "-Dcamel.main.name=my-app"); + + args = run("--profile=prod", "pom.xml").buildExistingCamelMainSystemProperties("my-app", logConfig); + assertThat(args).containsExactly( + "-Dlog4j2.configurationFile=" + logConfig.toAbsolutePath(), + "-Dcamel.main.name=my-app"); + } + + @Test + void quarkusLogsToFileViaJvmArgs() { + String logFile = CommandLineHelper.getCamelDir().resolve("my-app.log").toAbsolutePath().toString() + .replace("\\", "/"); + + List<String> args = run("--profile=dev", "--port=9090", "--prop=foo=bar", "--jvm-args=-Xmx512m", "pom.xml") + .buildExistingQuarkusJvmArgs("my-app"); + + assertThat(args).containsExactly( + "-Dquarkus.log.file.enabled=true", + "-Dquarkus.log.file.path=" + logFile, + "-Dquarkus.log.file.format=\"" + Run.QUARKUS_LOG_FILE_FORMAT + "\"", + "-Dcamel.main.profile=dev", + "-Dcamel.main.name=my-app", + "-Dquarkus.http.port=9090", + "-Dfoo=bar", + "-Xmx512m"); + } + + @Test + void springBootLogsToFileViaLogbackConfiguration() { + List<String> args = run("--profile=dev", "--port=9090", "--prop=foo=bar", "--jvm-args=-Xmx512m", "pom.xml") + .buildExistingSpringBootJvmArgs(); + + assertThat(args).containsExactly( + "-Dlogging.config=classpath:logback-camel-jbang.xml", + "-Dcamel.main.profile=dev", + "-Dserver.port=9090", + "-Dfoo=bar", + "-Xmx512m"); + } + + @Test + void jfrIsPassedAsJvmArgument() { + List<String> args = run("--jfr", "pom.xml").buildExistingQuarkusJvmArgs("my-app"); + assertThat(args).anyMatch(a -> a.startsWith("-XX:StartFlightRecording")); + + args = run("--jfr", "pom.xml").buildExistingSpringBootJvmArgs(); + assertThat(args).anyMatch(a -> a.startsWith("-XX:StartFlightRecording")); + } + +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java index 9454f5319860..dcaffdadce42 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java @@ -2385,7 +2385,6 @@ public class CamelMonitor extends CamelCommand { } } else { String pidStr = ctx.selectedPid; - actionsPopup.getLaunchManager().cleanupTempPom(pid); ProcessHandle.of(pid).ifPresent(ph -> { if (forceKill) { ph.destroyForcibly(); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java index 96566fa3f099..b3566400313a 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java @@ -16,15 +16,14 @@ */ package org.apache.camel.dsl.jbang.core.commands.tui; +import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.HashMap; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; @@ -41,7 +40,6 @@ class LaunchManager { private final Supplier<List<InfraInfo>> infraServices; private final List<PendingLaunch> pendingLaunches = new ArrayList<>(); - private final Map<Long, Path> activeTempPoms = new HashMap<>(); private DeferredLaunch deferredLaunch; private volatile String pendingAutoSelect; private BiConsumer<String, Boolean> notificationCallback; @@ -203,126 +201,30 @@ class LaunchManager { return false; } + /** + * Runs an existing Maven project via {@code camel run pom.xml}, which detects the runtime, injects the CLI + * connector, and logs to a file in {@code ~/.camel} that the Log tab reads (the same for all runtimes). + */ void launchMavenProject(String dir, String projectType, String displayName, List<String> extraArgs) { try { - List<String> cmd = new ArrayList<>(); - cmd.add(resolveMvnCommand(dir)); - switch (projectType) { - case "spring-boot" -> cmd.add("spring-boot:run"); - case "quarkus" -> cmd.add("quarkus:dev"); - default -> cmd.add("camel:run"); - } - // Translate Camel JBang args to Maven-compatible args - cmd.addAll(translateArgsForMaven(extraArgs, projectType)); - // Inject camel-cli-connector if not already in the project - Path tempPom = null; - if ("spring-boot".equals(projectType)) { - tempPom = injectCliConnectorIfMissing(dir, cmd); - } + List<String> cmd = new ArrayList<>(LauncherHelper.getCamelCommand()); + cmd.add("run"); + cmd.add(Path.of(dir, "pom.xml").toString()); + cmd.addAll(extraArgs); Path outputFile = createSecureTempFile("camel-maven-", ".log"); outputFile.toFile().deleteOnExit(); ProcessBuilder pb = new ProcessBuilder(cmd); - pb.directory(new java.io.File(dir)); + pb.directory(new File(dir)); pb.redirectErrorStream(true); pb.redirectOutput(outputFile.toFile()); Process process = pb.start(); - pendingLaunches.add(new PendingLaunch(displayName, process, outputFile, System.currentTimeMillis(), tempPom)); - if (pendingAutoSelect == null) { - pendingAutoSelect = displayName; - } - notify("Starting: " + displayName + " (mvn " + cmd.get(1) + ")", false); + addPendingLaunch(displayName, process, outputFile); + notify("Starting: " + displayName + " (" + projectType + ")", false); } catch (Exception e) { notify("Failed to start Maven project: " + e.getMessage(), true); } } - private Path injectCliConnectorIfMissing(String dir, List<String> cmd) { - try { - Path pomFile = Path.of(dir, "pom.xml"); - if (!Files.isRegularFile(pomFile)) { - return null; - } - String pomContent = Files.readString(pomFile); - if (pomContent.contains("camel-cli-connector")) { - return null; - } - // Add cli-connector-starter dependency (version managed by BOM) - String dep = "\n <dependency>\n" - + " <groupId>org.apache.camel.springboot</groupId>\n" - + " <artifactId>camel-cli-connector-starter</artifactId>\n" - + " </dependency>"; - // Find the project-level </dependencies> (not inside dependencyManagement or plugins) - int insertIdx = findProjectDependenciesEnd(pomContent); - if (insertIdx < 0) { - return null; - } - String modified = pomContent.substring(0, insertIdx) + dep + "\n " + pomContent.substring(insertIdx); - // Write temp pom in the project dir so Maven can find sources - Path tempPom = Path.of(dir, ".camel-tui-pom.xml"); - Files.writeString(tempPom, modified); - cmd.add("-f"); - cmd.add(tempPom.getFileName().toString()); - return tempPom; - } catch (Exception e) { - // best effort — don't fail the launch - return null; - } - } - - private static int findProjectDependenciesEnd(String pom) { - // Find <dependencies> that is a direct child of <project>, - // not nested inside <dependencyManagement>, <plugin>, or <profile> - int dmStart = pom.indexOf("<dependencyManagement>"); - int dmEnd = dmStart >= 0 ? pom.indexOf("</dependencyManagement>", dmStart) : -1; - int buildStart = pom.indexOf("<build>"); - - int searchFrom = 0; - while (true) { - int depStart = pom.indexOf("<dependencies>", searchFrom); - if (depStart < 0) { - return -1; - } - // Skip if inside <dependencyManagement> - if (dmStart >= 0 && depStart > dmStart && (dmEnd < 0 || depStart < dmEnd)) { - searchFrom = dmEnd > 0 ? dmEnd : depStart + 14; - continue; - } - // Skip if inside <build> (plugins can have dependencies) - if (buildStart >= 0 && depStart > buildStart) { - searchFrom = depStart + 14; - continue; - } - int depEnd = pom.indexOf("</dependencies>", depStart); - return depEnd >= 0 ? depEnd : -1; - } - } - - private static Path writeSpringBootLogbackConfig() { - try { - Path camelDir = Path.of(System.getProperty("user.home"), ".camel"); - Files.createDirectories(camelDir); - Path logbackFile = camelDir.resolve(".tui-logback-spring-boot.xml"); - String config = """ - <?xml version="1.0" encoding="UTF-8"?> - <configuration> - <include resource="org/springframework/boot/logging/logback/defaults.xml"/> - <include resource="org/springframework/boot/logging/logback/console-appender.xml"/> - <include resource="org/springframework/boot/logging/logback/file-appender.xml"/> - <property name="LOG_FILE" value="${user.home}${file.separator}.camel${file.separator}${PID}.log"/> - <property name="FILE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN}"/> - <root level="INFO"> - <appender-ref ref="CONSOLE"/> - <appender-ref ref="FILE"/> - </root> - </configuration> - """; - Files.writeString(logbackFile, config); - return logbackFile; - } catch (Exception e) { - return null; - } - } - void launchCamelRun(String sourceDir, String displayName, List<String> extraArgs) { try { List<String> cmd = new ArrayList<>(LauncherHelper.getCamelCommand()); @@ -343,67 +245,6 @@ class LaunchManager { } } - private static String resolveMvnCommand(String dir) { - String wrapper = System.getProperty("os.name", "").toLowerCase().contains("win") ? "mvnw.cmd" : "./mvnw"; - Path wrapperPath = Path.of(dir).resolve(wrapper.startsWith("./") ? wrapper.substring(2) : wrapper); - if (Files.isRegularFile(wrapperPath)) { - return wrapperPath.toString(); - } - return "mvn"; - } - - static List<String> translateArgsForMaven(List<String> extraArgs, String projectType) { - List<String> mvnArgs = new ArrayList<>(); - StringBuilder jvmArgs = new StringBuilder(); - if ("spring-boot".equals(projectType)) { - Path logbackFile = writeSpringBootLogbackConfig(); - if (logbackFile != null) { - jvmArgs.append("-Dlogging.config=file:").append(logbackFile); - } - } - for (String arg : extraArgs) { - if (arg.startsWith("--prop=")) { - String kv = arg.substring("--prop=".length()); - mvnArgs.add("-D" + kv); - } else if (arg.startsWith("--port=")) { - String port = arg.substring("--port=".length()); - if ("spring-boot".equals(projectType)) { - mvnArgs.add("-Dserver.port=" + port); - } else if ("quarkus".equals(projectType)) { - mvnArgs.add("-Dquarkus.http.port=" + port); - } - } else if (arg.startsWith("--profile=")) { - String profile = arg.substring("--profile=".length()); - if (!"prod".equals(profile)) { - if (!jvmArgs.isEmpty()) { - jvmArgs.append(" "); - } - jvmArgs.append("-Dcamel.main.profile=").append(profile); - } - } else if (arg.startsWith("--jvm-args=")) { - String extra = arg.substring("--jvm-args=".length()).trim(); - if (!extra.isEmpty()) { - if (!jvmArgs.isEmpty()) { - jvmArgs.append(" "); - } - jvmArgs.append(extra); - } - } - // other Camel JBang flags (--name, --runtime, --dev, --observe, etc.) - // are not applicable to Maven and are silently dropped - } - if (!jvmArgs.isEmpty()) { - if ("spring-boot".equals(projectType)) { - mvnArgs.add("-Dspring-boot.run.jvmArguments=" + jvmArgs); - } else if ("quarkus".equals(projectType)) { - mvnArgs.add("-Djvm.args=" + jvmArgs); - } else { - mvnArgs.add("-Dcamel.jvmArgs=" + jvmArgs); - } - } - return mvnArgs; - } - private void checkDeferredLaunch(long now) { if (deferredLaunch != null) { Set<String> runningAliases = infraServices.get().stream() @@ -434,50 +275,21 @@ class LaunchManager { failureLogCallback.accept(pl.name(), pl.outputFile()); } } - cleanupTempPom(pl); it.remove(); } else if (now - pl.startTime() > 8000) { notify("Started: " + pl.name(), false); - // keep temp pom reference for cleanup when process stops - if (pl.tempPom() != null) { - activeTempPoms.put(pl.process().pid(), pl.tempPom()); - } it.remove(); } } } - void cleanupTempPom(long pid) { - Path tempPom = activeTempPoms.remove(pid); - if (tempPom != null) { - try { - Files.deleteIfExists(tempPom); - } catch (Exception e) { - // best effort - } - } - } - - private static void cleanupTempPom(PendingLaunch pl) { - if (pl.tempPom() != null) { - try { - Files.deleteIfExists(pl.tempPom()); - } catch (Exception e) { - // best effort - } - } - } - private void notify(String msg, boolean error) { if (notificationCallback != null) { notificationCallback.accept(msg, error); } } - private record PendingLaunch(String name, Process process, Path outputFile, long startTime, Path tempPom) { - PendingLaunch(String name, Process process, Path outputFile, long startTime) { - this(name, process, outputFile, startTime, null); - } + private record PendingLaunch(String name, Process process, Path outputFile, long startTime) { } private record DeferredLaunch(
