This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch fix/CAMEL-25832
in repository https://gitbox.apache.org/repos/asf/camel.git

commit e749d34e40940c33cf51497008e2cea7f2f96292
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 20 10:10:52 2026 +0200

    CAMEL-25832: camel-jbang - camel run on the quarkus, spring-boot and main 
runtimes keeps the log of the previous run
    
    camel run kept the log of a run that ended only for the default runtime 
(CAMEL-25784). The Maven runtimes deleted it
    when the run ended or when the next run started, so camel log --previous 
and the AI tools had nothing to read about an
    app that failed to start.
    
    - When a run on quarkus, spring-boot or main ends (also camel run pom.xml), 
its <name>.log is kept as the previous run
      (PreviousRun.keep): with the pid and exit code of the app where camel run 
started it (java -jar, mvn camel:run), else
      "Maven ended with exit code N" when Maven, which runs the app in a 
process of its own, failed; 130/137/143 are said
      as the signal.
    - A <name>.log that a run left behind (such as one that was killed) is kept 
when the next run of that name starts,
      instead of being deleted.
    - Spring Boot logs to <name>.log as the other runtimes do, not <pid>.log; 
an existing project gets -Dcamel.main.name so
      the app reports the name of its log file, as Quarkus and Camel Main 
projects already do.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
---
 .../modules/ROOT/pages/camel-jbang-managing.adoc   |   8 +-
 .../apache/camel/dsl/jbang/core/commands/Run.java  | 165 ++++++++++++++-----
 .../camel/dsl/jbang/core/common/PreviousRun.java   |  30 ++++
 .../src/main/resources/spring-boot-logback.xml     |   3 +-
 .../core/commands/RunExistingProjectTest.java      |  17 +-
 .../core/commands/RunExportedProjectTest.java      |   2 +-
 .../commands/RunMavenRuntimePreviousLogTest.java   | 176 +++++++++++++++++++++
 7 files changed, 354 insertions(+), 47 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
index 4a7d5e65a271..7c0310bfda1d 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
@@ -652,8 +652,12 @@ The next run of that name that ends replaces it. Like 
Kubernetes, at most the la
 An integration that was killed, or whose JVM crashed, leaves its log behind.
 That log becomes the log of its previous run the next time you look one up.
 
-NOTE: The log is kept for the `jbang` runtime, which runs Camel in the JVM of 
the Camel CLI. This is the default, also
-with `--background`. The runtimes that run with Maven 
(`--runtime=spring-boot`, `quarkus` or `main`) do not keep it.
+The log is kept for every runtime: `jbang`, the default (also with 
`--background`), and the runtimes that run with
+Maven (`--runtime=spring-boot`, `quarkus` or `main`, and `camel run pom.xml` 
of an existing project).
+With `quarkus` and `spring-boot`, Maven runs the app in a process of its own, 
so the first line has no pid and no exit
+code of the app: it says how Maven ended when it failed, such as `Maven ended 
with exit code 1`.
+A run on a Maven runtime that was killed leaves its log behind as 
`~/.camel/<name>.log`. That log becomes the log of the
+previous run when the next run of that name starts.
 
 == Configuring logging levels
 
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 36b5e514b79d..abe4c7b06d0e 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
@@ -33,7 +33,6 @@ import java.nio.file.Files;
 import java.nio.file.InvalidPathException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
 import java.nio.file.StandardOpenOption;
 import java.text.ParseException;
 import java.util.*;
@@ -1718,11 +1717,8 @@ public class Run extends CamelCommand {
             // the process (and those it started) stops with this JVM, then 
the files of the run are removed
             ChildProcess.stopOnExit(processRef::get, () -> {
                 removeDir(runDirPath);
-                // cleanup log file
-                String appName = appNameRef.get();
-                if (appName != null) {
-                    
Files.deleteIfExists(CommandLineHelper.getCamelDir().resolve(appName + ".log"));
-                }
+                // the log is kept as the previous run of the app (CAMEL-25832)
+                keepRunLog(appNameRef.get(), processRef.get(), false);
             });
         }
         Files.createDirectories(runDirPath);
@@ -1786,6 +1782,7 @@ public class Run extends CamelCommand {
         // must be named after the name the application reports (which is what 
camel log and the TUI look for)
         String appName = resolveExportedAppName(runDirPath, eq.name);
         appNameRef.set(appName);
+        keepLeftoverRunLog(appName);
 
         // prepare quarkus for logging to file
         Path appProps = Paths.get(eq.exportDir, 
"src/main/resources/application.properties");
@@ -1874,6 +1871,8 @@ public class Run extends CamelCommand {
 
         AtomicReference<Process> processRef = new AtomicReference<>();
         AtomicReference<String> appNameRef = new AtomicReference<>();
+        // the JVM of the app, once the project is built
+        AtomicReference<Process> appRef = new AtomicReference<>();
 
         // create temp run dir
         Path runDirPath = Paths.get(RUN_PLATFORM_DIR, 
Long.toString(System.currentTimeMillis()));
@@ -1881,10 +1880,9 @@ public class Run extends CamelCommand {
         // the process (and those it started) stops with this JVM, then the 
files of the run are removed
         ChildProcess.stopOnExit(processRef::get, () -> {
             removeDir(runDirPath);
-            // cleanup log file
-            String appName = appNameRef.get();
-            if (appName != null) {
-                
Files.deleteIfExists(CommandLineHelper.getCamelDir().resolve(appName + ".log"));
+            // the log is kept as the previous run of the app (CAMEL-25832); a 
build that failed has no log
+            if (appRef.get() != null) {
+                keepRunLog(appNameRef.get(), appRef.get(), true);
             }
         });
         Files.createDirectories(runDirPath);
@@ -2001,6 +1999,7 @@ public class Run extends CamelCommand {
         pb.inheritIO(); // run in foreground (with IO so logs are visible)
         p = pb.start();
         processRef.set(p);
+        appRef.set(p);
         this.spawnPid = p.pid();
         // wait for that process to exit as we run in foreground
         return p.waitFor();
@@ -2092,12 +2091,12 @@ public class Run extends CamelCommand {
 
     /**
      * 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).
+     * console, and to the {@code <name>.log} file in the {@code ~/.camel} 
directory. A log a run of that name left
+     * behind is kept as its previous run first, so the new log does not start 
with the output of that run.
      */
     static void writeCamelMainRunLogConfig(Path target, String appName, String 
loggingLevel) throws IOException {
-        Path logFile = CommandLineHelper.getCamelDir().resolve(appName + 
".log");
-        Files.deleteIfExists(logFile);
+        Path logFile = runLogFile(appName);
+        keepLeftoverRunLog(appName);
         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)
@@ -2108,6 +2107,80 @@ public class Run extends CamelCommand {
         }
     }
 
+    /**
+     * The log a run on a Maven runtime writes in the Camel directory: {@code 
<name>.log}, which {@code camel log} and
+     * the TUI read when there is no {@code <pid>.log}.
+     */
+    static Path runLogFile(String appName) {
+        return CommandLineHelper.getCamelDir().resolve(appName + ".log");
+    }
+
+    /**
+     * A log that a run of this name left behind, such as a run that was 
killed so it could not keep its log, is kept as
+     * the previous run of the name before the new run writes its own 
(CAMEL-25832).
+     */
+    static void keepLeftoverRunLog(String appName) {
+        PreviousRun.keepLeftover(runLogFile(appName), appName);
+    }
+
+    /**
+     * When a run on a Maven runtime ends, its log is kept as the previous run 
of its name, as camel run does for the
+     * default runtime (CAMEL-25784), so camel log --previous and the AI tools 
can read why the app stopped, such as an
+     * app that failed to start (CAMEL-25832).
+     *
+     * @param appName the name of the app, or null when the run did not get 
that far
+     * @param process the last process of the run, which has stopped
+     * @param app     whether the process is the app itself (java -jar, or mvn 
camel:run, where Camel runs in the Maven
+     *                JVM), or Maven that runs the app in a process of its own 
(quarkus:run, quarkus:dev,
+     *                spring-boot:run), whose pid and exit code are not those 
of the app
+     */
+    static void keepRunLog(String appName, Process process, boolean app) {
+        Integer code = process != null && !process.isAlive() ? 
process.exitValue() : null;
+        keepRunLog(appName, app && process != null ? process.pid() : 0, code, 
app);
+    }
+
+    static Path keepRunLog(String appName, long pid, Integer code, boolean 
app) {
+        if (appName == null) {
+            return null;
+        }
+        String signal = code != null ? signalOf(code) : null;
+        // the exit code of Maven is not the one of the app: said as such, 
when it failed
+        Integer exitCode = app || signal != null ? code : null;
+        String failure = !app && code != null && code != 0 && signal == null ? 
"Maven ended with exit code " + code : null;
+        return PreviousRun.keep(runLogFile(appName), appName, pid, exitCode, 
signal, failure);
+    }
+
+    /**
+     * The signal a process stopped by it exits with, as 128 and the number of 
the signal: 130 for SIGINT (Ctrl+C), 137
+     * for SIGKILL and 143 for SIGTERM; null for another exit code.
+     */
+    static String signalOf(int exitCode) {
+        return switch (exitCode) {
+            case 130 -> "SIGINT";
+            case 137 -> "SIGKILL";
+            case 143 -> "SIGTERM";
+            default -> null;
+        };
+    }
+
+    /**
+     * Writes the logback configuration used by {@code camel run 
--runtime=spring-boot} to the given file. It logs to
+     * the console, and to the {@code <name>.log} file in the Camel directory, 
as the other runtimes do, so the log can
+     * be kept as the previous run of the app (CAMEL-25832).
+     */
+    static void writeSpringBootLogbackConfig(Path target, String appName) 
throws IOException {
+        String fileName = 
runLogFile(appName).toAbsolutePath().toString().replace("\\", "/")
+                .replace("&", "&amp;").replace("<", "&lt;").replace("\"", 
"&quot;");
+        try (InputStream is = 
Run.class.getClassLoader().getResourceAsStream("spring-boot-logback.xml")) {
+            if (is == null) {
+                throw new IOException("spring-boot-logback.xml not found");
+            }
+            String content = new String(is.readAllBytes(), 
StandardCharsets.UTF_8).replace("{{logFile}}", fileName);
+            Files.createDirectories(target.getParent());
+            Files.writeString(target, content);
+        }
+    }
+
     /**
      * --data-dir only changes what dev mode watches in a source directory: 
without one only the given files are
      * watched, and their data folders are not, so the option has nothing to 
leave out. Said when it is given on the
@@ -2245,6 +2318,8 @@ public class Run extends CamelCommand {
         ChildProcess.stopOnExit(processRef::get, () -> {
             Files.deleteIfExists(tempPom);
             Files.deleteIfExists(logConfig);
+            // the log is kept as the previous run of the app, which ran in 
the Maven JVM (CAMEL-25832)
+            keepRunLog(appName, processRef.get(), true);
         });
 
         // find mvnw or fall back to mvn
@@ -2331,19 +2406,21 @@ 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);
+        keepLeftoverRunLog(appName);
+
         // shutdown hook to clean up temp files
         // the process (and those it started) stops with this JVM, then the 
files of the run are removed
         ChildProcess.stopOnExit(processRef::get, () -> {
             Files.deleteIfExists(tempPom);
+            // the log is kept as the previous run of the app (CAMEL-25832)
+            keepRunLog(appName, processRef.get(), false);
         });
 
         // find mvnw or fall back to mvn
         String mvnCmd = MavenCommand.of(projectDir);
 
-        // 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");
@@ -2399,6 +2476,8 @@ public class Run extends CamelCommand {
             return runExistingSpringBootProject(processRef);
         }
 
+        AtomicReference<String> appNameRef = new AtomicReference<>();
+
         // create temp run dir
         Path runDirPath = Paths.get(RUN_PLATFORM_DIR, 
Long.toString(System.currentTimeMillis()));
         if (!this.background) {
@@ -2406,6 +2485,8 @@ public class Run extends CamelCommand {
             // the process (and those it started) stops with this JVM, then 
the files of the run are removed
             ChildProcess.stopOnExit(processRef::get, () -> {
                 removeDir(runDirPath);
+                // the log is kept as the previous run of the app (CAMEL-25832)
+                keepRunLog(appNameRef.get(), processRef.get(), false);
             });
         }
         Files.createDirectories(runDirPath);
@@ -2475,12 +2556,14 @@ public class Run extends CamelCommand {
             return exit;
         }
 
+        // the exported project may have derived the application name from the 
source files, and the log file is
+        // named after the name the application reports (what camel log and 
the TUI look for)
+        String appName = resolveExportedAppName(runDirPath, eq.name);
+        appNameRef.set(appName);
+        keepLeftoverRunLog(appName);
+
         // prepare spring-boot for logging to file
-        try (InputStream is = 
Run.class.getClassLoader().getResourceAsStream("spring-boot-logback.xml")) {
-            Path logbackPath = Paths.get(eq.exportDir, 
"src/main/resources/logback.xml");
-            Files.createDirectories(logbackPath.getParent());
-            Files.copy(is, logbackPath, StandardCopyOption.REPLACE_EXISTING);
-        }
+        writeSpringBootLogbackConfig(Paths.get(eq.exportDir, 
"src/main/resources/logback.xml"), appName);
 
         // run spring-boot via maven
         ProcessBuilder pb = new ProcessBuilder();
@@ -2508,22 +2591,16 @@ public class Run extends CamelCommand {
     }
 
     /**
-     * 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}.
+     * The logback configuration an existing Spring Boot project runs with, 
logging to the {@code <name>.log} file so
+     * the TUI can read the log: a temp file, so the project is not changed 
and needs no src/main/resources. Null when
+     * it cannot be written.
      */
-    /**
-     * The logback configuration an existing Spring Boot project runs with, 
logging to file so the TUI can read the log:
-     * a temp file, so the project is not changed and needs no 
src/main/resources. Null when it cannot be written.
-     */
-    Path springBootLogbackConfig() {
+    Path springBootLogbackConfig(String appName) {
         if (springBootLogback == null) {
-            try (InputStream is = 
Run.class.getClassLoader().getResourceAsStream("spring-boot-logback.xml")) {
-                if (is == null) {
-                    return null;
-                }
+            try {
                 Path file = Files.createTempFile("camel-jbang-logback-", 
".xml");
                 file.toFile().deleteOnExit();
-                Files.copy(is, file, StandardCopyOption.REPLACE_EXISTING);
+                writeSpringBootLogbackConfig(file, appName);
                 springBootLogback = file;
             } catch (IOException e) {
                 return null;
@@ -2532,15 +2609,21 @@ public class Run extends CamelCommand {
         return springBootLogback;
     }
 
-    List<String> buildExistingSpringBootJvmArgs() {
+    /**
+     * The JVM arguments ({@code spring-boot.run.jvmArguments}) an existing 
Spring Boot project is run with: logging to
+     * file, the profile, the name (so the app reports the name of its log 
file), the port and properties, the flight
+     * recording and {@code --jvm-args}.
+     */
+    List<String> buildExistingSpringBootJvmArgs(String appName) {
         List<String> args = new ArrayList<>();
-        Path logback = springBootLogbackConfig();
+        Path logback = springBootLogbackConfig(appName);
         if (logback != null) {
             args.add("-Dlogging.config=" + logback.toUri());
         }
         if (profile != null && !"prod".equals(profile)) {
             args.add("-Dcamel.main.profile=" + profile);
         }
+        args.add("-Dcamel.main.name=" + appName);
         args.addAll(buildExistingProjectSystemProperties("server.port"));
         String jfrArg = buildJfrJvmArgs();
         if (jfrArg != null) {
@@ -2589,6 +2672,10 @@ 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);
+        keepLeftoverRunLog(appName);
+
         // shutdown hook to clean up temp files
         // the process (and those it started) stops with this JVM, then the 
files of the run are removed
         ChildProcess.stopOnExit(processRef::get, () -> {
@@ -2596,6 +2683,8 @@ public class Run extends CamelCommand {
             if (springBootLogback != null) {
                 Files.deleteIfExists(springBootLogback);
             }
+            // the log is kept as the previous run of the app (CAMEL-25832)
+            keepRunLog(appName, processRef.get(), false);
         });
 
         // find mvnw or fall back to mvn
@@ -2606,7 +2695,7 @@ public class Run extends CamelCommand {
         cmd.add("--quiet");
         cmd.add("--file");
         cmd.add(tempPom.toString());
-        cmd.add("-Dspring-boot.run.jvmArguments=" + String.join(" ", 
buildExistingSpringBootJvmArgs()));
+        cmd.add("-Dspring-boot.run.jvmArguments=" + String.join(" ", 
buildExistingSpringBootJvmArgs(appName)));
         cmd.add("-DskipTests");
         cmd.add("package");
         cmd.add("spring-boot:run");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PreviousRun.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PreviousRun.java
index 03e7bbac8f80..7991c5f248d7 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PreviousRun.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PreviousRun.java
@@ -150,6 +150,36 @@ public final class PreviousRun {
         return keep(log, thisRun(name, pid, exitCode, null, failure));
     }
 
+    /**
+     * Keeps the log of a run that ended as the previous log of its name, with 
the signal that stopped it, such as a run
+     * of a process that this JVM started (CAMEL-25832).
+     *
+     * @param  log      the log of the run
+     * @param  name     the name of the integration
+     * @param  pid      the pid it ran as, 0 when not known
+     * @param  exitCode the exit code, or <tt>null</tt> when not known
+     * @param  signal   the signal that stopped the run, such as 
<tt>SIGINT</tt>, or <tt>null</tt>
+     * @param  failure  why it ended, when it failed, or <tt>null</tt>
+     * @return          the kept log, or <tt>null</tt> when there was no log 
to keep
+     */
+    public static Path keep(Path log, String name, long pid, Integer exitCode, 
String signal, String failure) {
+        return keep(log, thisRun(name, pid, exitCode, signal, failure));
+    }
+
+    /**
+     * Keeps a log that a run of this name left behind, such as a run that was 
killed so it could not keep its log, as
+     * the previous log of its name (CAMEL-25832). How that run ended is not 
known: the last write of its log is the
+     * closest time to when it ended.
+     *
+     * @return the kept log, or <tt>null</tt> when there was no log to keep
+     */
+    public static Path keepLeftover(Path log, String name) {
+        if (log == null || !Files.isRegularFile(log)) {
+            return null;
+        }
+        return keep(log, new Info(name, 0, null, 0, 
log.toFile().lastModified(), null, null, null, null));
+    }
+
     // a run of this JVM that ends now
     private static Info thisRun(String name, long pid, Integer exitCode, 
String signal, String failure) {
         long started = 
ProcessHandle.current().info().startInstant().map(Instant::toEpochMilli).orElse(0L);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/spring-boot-logback.xml 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/spring-boot-logback.xml
index 8e29eaa77b5c..b1c741e07b1e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/spring-boot-logback.xml
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/spring-boot-logback.xml
@@ -21,7 +21,8 @@
     <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"/>
+    <!-- the <name>.log file in the Camel directory, as camel run writes in 
this file (CAMEL-25832) -->
+    <property name="LOG_FILE" value="{{logFile}}"/>
     <property name="FILE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN}"/> <!-- 
log in color to file -->
     <root level="INFO">
         <appender-ref ref="CONSOLE"/>
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
index 22cc293cabdc..1ede0bc4c47d 100644
--- 
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
@@ -94,8 +94,9 @@ class RunExistingProjectTest extends 
CamelCommandBaseTestSupport {
 
         Run.writeCamelMainRunLogConfig(logConfig, "my-app", "debug");
 
-        // the previous log is removed so the readers do not show stale lines
+        // the log of the last run is kept as its previous run (CAMEL-25832), 
so the readers do not show stale lines
         assertThat(logFile).doesNotExist();
+        
assertThat(CommandLineHelper.getCamelDir().resolve("my-app-previous.log")).hasContent("stale
 output of a previous run");
         String content = Files.readString(logConfig);
         assertThat(content).contains("appender.file.fileName = " + 
logFile.toAbsolutePath().toString().replace("\\", "/"));
         assertThat(content).contains("rootLogger.level = debug");
@@ -156,15 +157,21 @@ class RunExistingProjectTest extends 
CamelCommandBaseTestSupport {
 
     @Test
     void springBootLogsToFileViaLogbackConfiguration() {
+        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")
-                .buildExistingSpringBootJvmArgs();
+                .buildExistingSpringBootJvmArgs("my-app");
 
-        // the logback configuration is a temp file: the project needs no 
src/main/resources and is not changed
+        // the logback configuration is a temp file: the project needs no 
src/main/resources and is not changed; it logs
+        // to <name>.log, as the other runtimes do, so the log is kept as the 
previous run (CAMEL-25832)
         
assertThat(args.get(0)).startsWith("-Dlogging.config=file:").endsWith(".xml");
         
assertThat(Path.of(URI.create(args.get(0).substring("-Dlogging.config=".length())))).exists()
-                .content().contains("<configuration");
+                .content().contains("<configuration").contains("value=\"" + 
logFile + "\"").doesNotContain("{{");
+        // the name is passed, so the app reports the name of its log file
         assertThat(args.subList(1, args.size())).containsExactly(
                 "-Dcamel.main.profile=dev",
+                "-Dcamel.main.name=my-app",
                 "-Dserver.port=9090",
                 "-Dfoo=bar",
                 "-Xmx512m");
@@ -175,7 +182,7 @@ class RunExistingProjectTest extends 
CamelCommandBaseTestSupport {
         List<String> args = run("--jfr", 
"pom.xml").buildExistingQuarkusJvmArgs("my-app");
         assertThat(args).anyMatch(a -> 
a.startsWith("-XX:StartFlightRecording"));
 
-        args = run("--jfr", "pom.xml").buildExistingSpringBootJvmArgs();
+        args = run("--jfr", 
"pom.xml").buildExistingSpringBootJvmArgs("my-app");
         assertThat(args).anyMatch(a -> 
a.startsWith("-XX:StartFlightRecording"));
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunExportedProjectTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunExportedProjectTest.java
index a5be3bc8f9c1..a3d5f769c362 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunExportedProjectTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunExportedProjectTest.java
@@ -79,7 +79,7 @@ class RunExportedProjectTest extends 
CamelCommandBaseTestSupport {
         assertThat(run.buildExportedRunJvmArgs()).isEqualTo(String.join(" ", 
limits));
         // existing project (camel run pom.xml), per runtime
         
assertThat(run.buildExistingQuarkusJvmArgs("my-app")).containsAll(limits);
-        assertThat(run.buildExistingSpringBootJvmArgs()).containsAll(limits);
+        
assertThat(run.buildExistingSpringBootJvmArgs("my-app")).containsAll(limits);
         assertThat(run.buildExistingCamelMainSystemProperties("my-app", 
dir.resolve("log4j2.properties")))
                 .containsAll(limits);
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunMavenRuntimePreviousLogTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunMavenRuntimePreviousLogTest.java
new file mode 100644
index 000000000000..56c161ec197f
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunMavenRuntimePreviousLogTest.java
@@ -0,0 +1,176 @@
+/*
+ * 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.nio.file.attribute.FileTime;
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.PreviousRun;
+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 static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-25832: camel run on a Maven runtime (quarkus, spring-boot, main, or 
an existing project) keeps the log of the
+ * app as its previous run when the run ends, as it does for the default 
runtime (CAMEL-25784).
+ */
+@Isolated
+class RunMavenRuntimePreviousLogTest {
+
+    @TempDir
+    Path home;
+
+    private String originalHome;
+    private Path camelDir;
+
+    @BeforeEach
+    void useTempHome() throws Exception {
+        originalHome = CommandLineHelper.getHomeDir().toString();
+        CommandLineHelper.useHomeDir(home.toString());
+        camelDir = CommandLineHelper.getCamelDir();
+        Files.createDirectories(camelDir);
+    }
+
+    @AfterEach
+    void restoreHome() {
+        CommandLineHelper.useHomeDir(originalHome);
+    }
+
+    private Path log(String name, String content) throws Exception {
+        Path log = camelDir.resolve(name + ".log");
+        Files.writeString(log, content);
+        return log;
+    }
+
+    private static PreviousRun.Info previous(String name) {
+        List<PreviousRun.Info> found = PreviousRun.find(name + "!");
+        assertThat(found).hasSize(1);
+        return found.get(0);
+    }
+
+    @Test
+    void theLogOfAnAppThatFailedIsKeptWithItsPidAndExitCode() throws Exception 
{
+        // java -jar of --runtime=main, or mvn camel:run where Camel runs in 
the Maven JVM
+        Path log = log("orders", "ERROR Failed to start route orders");
+
+        Path kept = Run.keepRunLog("orders", 4242, 1, true);
+
+        assertThat(log).doesNotExist();
+        assertThat(kept).hasContent("ERROR Failed to start route orders");
+        PreviousRun.Info info = previous("orders");
+        assertThat(info.pid()).isEqualTo(4242);
+        assertThat(info.exitCode()).isEqualTo(1);
+        assertThat(info.signal()).isNull();
+        assertThat(info.failure()).isNull();
+    }
+
+    @Test
+    void theExitCodeOfMavenIsNotGivenAsTheOneOfTheApp() throws Exception {
+        // quarkus:run, quarkus:dev and spring-boot:run run the app in a 
process of its own
+        log("orders", "ERROR Application run failed");
+
+        Run.keepRunLog("orders", 0, 1, false);
+
+        PreviousRun.Info info = previous("orders");
+        assertThat(info.pid()).isZero();
+        assertThat(info.exitCode()).isNull();
+        assertThat(info.failure()).isEqualTo("Maven ended with exit code 1");
+        assertThat(info.describe()).contains("The previous run of orders ended 
at").endsWith(": Maven ended with exit code 1");
+    }
+
+    @Test
+    void aRunThatCtrlCStoppedEndsWithTheSignal() throws Exception {
+        log("orders", "INFO Apache Camel 4.24.0 (orders) shutdown");
+
+        Run.keepRunLog("orders", 0, 130, false);
+
+        PreviousRun.Info info = previous("orders");
+        assertThat(info.exitCode()).isEqualTo(130);
+        assertThat(info.signal()).isEqualTo("SIGINT");
+        assertThat(info.failure()).isNull();
+        assertThat(info.describe()).endsWith("with exit code 130 (SIGINT)");
+    }
+
+    @Test
+    void aRunThatEndedWellHasNoFailure() throws Exception {
+        log("orders", "INFO Apache Camel 4.24.0 (orders) shutdown");
+
+        Run.keepRunLog("orders", 0, 0, false);
+
+        PreviousRun.Info info = previous("orders");
+        assertThat(info.exitCode()).isNull();
+        assertThat(info.failure()).isNull();
+    }
+
+    @Test
+    void withoutALogOrANameNothingIsKept() throws Exception {
+        // a build that failed before the app ran, or a run that stopped 
before it had a name
+        assertThat(Run.keepRunLog("orders", 0, 1, false)).isNull();
+        assertThat(Run.keepRunLog(null, 0, 1, false)).isNull();
+        assertThat(PreviousRun.findAll()).isEmpty();
+    }
+
+    @Test
+    void aLogARunLeftBehindIsKeptWhenTheNextRunStarts() throws Exception {
+        // a run that was killed could not keep its log
+        Path log = log("orders", "INFO the run before");
+        // whole seconds, as some file systems keep no milliseconds
+        long modified = (System.currentTimeMillis() / 1000 - 60) * 1000;
+        Files.setLastModifiedTime(log, FileTime.fromMillis(modified));
+
+        Run.keepLeftoverRunLog("orders");
+
+        assertThat(log).doesNotExist();
+        PreviousRun.Info info = previous("orders");
+        assertThat(info.log()).hasContent("INFO the run before");
+        // how it ended is not known: the last write of its log is the closest 
time
+        assertThat(info.ended()).isEqualTo(modified);
+        assertThat(info.exitCode()).isNull();
+        assertThat(info.failure()).isNull();
+
+        // nothing left behind: the previous run stays as it is
+        Run.keepLeftoverRunLog("orders");
+        assertThat(previous("orders").log()).hasContent("INFO the run before");
+    }
+
+    @Test
+    void theSignalOfAnExitCode() {
+        assertThat(Run.signalOf(130)).isEqualTo("SIGINT");
+        assertThat(Run.signalOf(137)).isEqualTo("SIGKILL");
+        assertThat(Run.signalOf(143)).isEqualTo("SIGTERM");
+        assertThat(Run.signalOf(0)).isNull();
+        assertThat(Run.signalOf(1)).isNull();
+    }
+
+    @Test
+    void springBootLogsToTheLogOfItsName() throws Exception {
+        Path logback = home.resolve("export/src/main/resources/logback.xml");
+
+        Run.writeSpringBootLogbackConfig(logback, "orders");
+
+        String logFile = 
camelDir.resolve("orders.log").toAbsolutePath().toString().replace("\\", "/");
+        assertThat(logback).content().contains("<property name=\"LOG_FILE\" 
value=\"" + logFile + "\"/>")
+                .doesNotContain("{{").doesNotContain("${PID}");
+    }
+}

Reply via email to