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 8f30fa7f5267 CAMEL-25818: camel-jbang - the console output of 
camel_run and camel_control restart goes to <pid>-run.log and is removed when 
the integration ends (#28116)
8f30fa7f5267 is described below

commit 8f30fa7f5267efa1fbc81dc68cbd1cd0eab98088
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 19 19:02:32 2026 +0200

    CAMEL-25818: camel-jbang - the console output of camel_run and 
camel_control restart goes to <pid>-run.log and is removed when the integration 
ends (#28116)
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   8 ++
 .../core/commands/ai/IntegrationLauncher.java      |  70 +++++++---
 .../ai/IntegrationLauncherConsoleTest.java         | 146 +++++++++++++++++++++
 3 files changed, 203 insertions(+), 21 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index bf551ed44027..be17cc8fe5c7 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -1800,3 +1800,11 @@ Content with more than one reading is refused as before.
 to the closest lines of the file when exactly one place matches well. It 
changes there the lines the model changed and
 keeps the file's version of the other lines. The edit stands only when its 
result is valid; otherwise the answer is
 the one of a miss, with the lines the file has there. `matchedLines` in the 
answer names the lines it matched.
+
+=== camel-jbang - the console output of an integration that the AI tools start
+
+`camel_run` and `camel_control restart` send the console output of the 
integration they start to
+`~/.camel/<pid>-run.log`, as `camel run --background` does, and remove it when 
the integration ends. It went to
+`~/.camel/camel-launch-<number>.log` or `camel-restart-<number>.log`, which 
stayed until the MCP server exited. An
+integration that still runs when the server exits keeps the file while it 
runs: `camel stop` removes it, and
+`camel dirty --clean` once the integration has ended.
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
index 031b5a1fc738..b666032ab2b5 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
@@ -28,10 +28,11 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.BackgroundLog;
 import org.apache.camel.dsl.jbang.core.common.LauncherHelper;
 import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
 import org.apache.camel.util.json.JsonArray;
@@ -83,6 +84,11 @@ public final class IntegrationLauncher {
      */
     private static final Map<Path, String> STOPPED_BY_RESTART = new 
ConcurrentHashMap<>();
 
+    /**
+     * The camel command that integrations are started with; a test starts 
another program instead.
+     */
+    static Supplier<List<String>> camelCommand = 
LauncherHelper::getCamelCommand;
+
     private IntegrationLauncher() {
     }
 
@@ -100,7 +106,7 @@ public final class IntegrationLauncher {
     public static JsonObject run(Path directory, List<String> files, String 
name, boolean dev, List<String> extraArgs) {
         String runName = runName(directory, name);
         boolean before = runName != null && !runName.equals(name);
-        List<String> cmd = new ArrayList<>(LauncherHelper.getCamelCommand());
+        List<String> cmd = new ArrayList<>(camelCommand.get());
         // no files given: the whole directory is the app (camel run 
--source-dir), so a file the agent adds later,
         // a bean file, a Java class under src/main/java, is part of it and 
reloaded in dev mode (CAMEL-24861);
         // with files given only those run, for a directory that holds several 
apps
@@ -129,14 +135,12 @@ public final class IntegrationLauncher {
             }
             result.put("command", String.join(" ", cmd));
             try {
-                Files.createDirectories(CommandLineHelper.getCamelDir());
-                output = Files.createTempFile(CommandLineHelper.getCamelDir(), 
"camel-launch-", ".log");
-                output.toFile().deleteOnExit();
                 ProcessBuilder pb = new ProcessBuilder(cmd);
                 pb.directory(directory.toFile());
-                pb.redirectErrorStream(true);
-                pb.redirectOutput(output.toFile());
-                process = pb.start();
+                // the console output goes to ~/.camel/<pid>-run.log, as for 
camel run --background (CAMEL-25818)
+                BackgroundLog.Started launched = BackgroundLog.start(pb);
+                process = launched.process();
+                output = launched.log();
             } catch (IOException e) {
                 result.put("status", "failed");
                 result.put("error", "Cannot start camel run: " + 
e.getMessage());
@@ -160,6 +164,7 @@ public final class IntegrationLauncher {
                 result.put("message", cause != null
                         ? "camel run exited before the integration started: " 
+ cause
                         : "camel run exited before the integration started; 
the output has the reason");
+                removeConsoleOutput(output);
                 return result;
             }
             RuntimeHelper.ProcessInfo info = findByPid(pid);
@@ -185,6 +190,7 @@ public final class IntegrationLauncher {
                                                       : "; dev mode reloads 
the routes when a source file changes"
                                               : "; restart it after changing a 
source file")
                                       + ". camel_get_log reads its log, 
camel_get_errors its failed exchanges.");
+                removeWhenEnded(process, output);
                 return result;
             }
             try {
@@ -199,6 +205,7 @@ public final class IntegrationLauncher {
         result.put("output", readTail(output));
         result.put("message", "camel run is still starting after " + 
STARTUP_TIMEOUT_MS / 1000
                               + "s (dependencies may be downloading); 
list_processes shows it once it is up");
+        removeWhenEnded(process, output);
         return result;
     }
 
@@ -603,19 +610,21 @@ public final class IntegrationLauncher {
                 // the relaunch below is attempted anyway
             }
         }
+        // the console output of the process that stopped, as camel restart 
removes it
+        BackgroundLog.remove(pid);
         try {
-            Path output = 
Files.createTempFile(CommandLineHelper.getCamelDir(), "camel-restart-", ".log");
-            output.toFile().deleteOnExit();
             ProcessBuilder pb = new ProcessBuilder(cmd);
             if (directory != null) {
                 pb.directory(directory.toFile());
             }
-            pb.redirectErrorStream(true);
-            pb.redirectOutput(output.toFile());
-            Process p = pb.start();
+            // the console output goes to ~/.camel/<pid>-run.log, as for camel 
restart (CAMEL-25818)
+            BackgroundLog.Started launched = BackgroundLog.start(pb);
+            Process p = launched.process();
+            Path output = launched.log();
             long deadline = System.currentTimeMillis() + STARTUP_TIMEOUT_MS;
             while (System.currentTimeMillis() < deadline && p.isAlive()) {
                 if (findByPid(p.pid()) != null) {
+                    removeWhenEnded(p, output);
                     return "Restarted " + (name != null ? name : "pid " + pid) 
+ " as pid " + p.pid();
                 }
                 Thread.sleep(250);
@@ -623,15 +632,16 @@ public final class IntegrationLauncher {
             if (!p.isAlive()) {
                 boolean kept = stoppedByRestart(directory, name);
                 String cause = failureCause(output);
-                throw new ToolExecutionException(
-                        "Restart of " + (name != null ? name : "pid " + pid) + 
" failed with exit code "
-                                                 + p.exitValue() + ", so it is 
stopped now"
-                                                 + (cause != null
-                                                         ? ". The cause: " + 
cause + "\nFix it" : ": fix the cause below")
-                                                 + ", then camel_run starts it 
again"
-                                                 + (kept ? " (directory " + 
directory + ") under the same name, " + name : "")
-                                                 + ".\n" + 
failureOutput(output));
+                String failure = "Restart of " + (name != null ? name : "pid " 
+ pid) + " failed with exit code "
+                                 + p.exitValue() + ", so it is stopped now"
+                                 + (cause != null ? ". The cause: " + cause + 
"\nFix it" : ": fix the cause below")
+                                 + ", then camel_run starts it again"
+                                 + (kept ? " (directory " + directory + ") 
under the same name, " + name : "")
+                                 + ".\n" + failureOutput(output);
+                removeConsoleOutput(output);
+                throw new ToolExecutionException(failure);
             }
+            removeWhenEnded(p, output);
             return "Restarting " + (name != null ? name : "pid " + pid) + " as 
pid " + p.pid()
                    + " (still starting)";
         } catch (IOException e) {
@@ -642,6 +652,24 @@ public final class IntegrationLauncher {
         }
     }
 
+    /**
+     * Removes the console output of the process once the process has ended 
(CAMEL-25818). The tools read it only to
+     * answer (the cause of a start that failed, or the output of a run that 
is still starting), and the log of the
+     * integration has what it logged. An integration that outlives this JVM 
keeps it while it runs, as one run with
+     * camel run --background does: camel stop removes it, and camel dirty 
once the process is gone.
+     */
+    static void removeWhenEnded(Process process, Path output) {
+        process.onExit().thenRun(() -> removeConsoleOutput(output));
+    }
+
+    private static void removeConsoleOutput(Path output) {
+        try {
+            Files.deleteIfExists(output);
+        } catch (IOException e) {
+            // camel stop and camel dirty remove it
+        }
+    }
+
     /** The working directory of a camel run process, which it reports in the 
runtime section of its status file. */
     private static Path workingDirectory(long pid) {
         JsonObject status = RuntimeHelper.readStatus(pid);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherConsoleTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherConsoleTest.java
new file mode 100644
index 000000000000..a205995903de
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherConsoleTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.ai;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.List;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+/**
+ * CAMEL-25818: the console output of an integration that camel_run or 
camel_control restart started goes to
+ * <tt>~/.camel/&lt;pid&gt;-run.log</tt>, as for camel run --background, and 
is removed when the integration ends. It
+ * stayed in a file of its own until this JVM exited.
+ */
+@DisabledOnOs(value = OS.WINDOWS, disabledReason = "the integrations are shell 
scripts")
+class IntegrationLauncherConsoleTest {
+
+    // an integration: it writes its pid file and status file, as the CLI 
connector does, and runs until its pid file is
+    // removed, as camel stop does
+    private static final String RUNNING = "d=DIR; touch \"$d/$$\"; echo 
started;"
+                                          + " echo 
'{\"context\":{\"name\":\"orders\",\"phase\":5,\"state\":\"Started\"}}'"
+                                          + " > \"$d/$$-status.json\";"
+                                          + " while [ -f \"$d/$$\" ]; do sleep 
0.1; done; rm -f \"$d/$$-status.json\"";
+
+    @TempDir
+    Path home;
+    @TempDir
+    Path project;
+
+    private Path previousHome;
+    private Supplier<List<String>> previousCommand;
+    private Path camelDir;
+
+    @BeforeEach
+    void useHome() throws IOException {
+        previousHome = CommandLineHelper.getHomeDir();
+        CommandLineHelper.useHomeDir(home.toString());
+        camelDir = CommandLineHelper.getCamelDir();
+        Files.createDirectories(camelDir);
+        previousCommand = IntegrationLauncher.camelCommand;
+    }
+
+    @AfterEach
+    void restore() {
+        IntegrationLauncher.camelCommand = previousCommand;
+        CommandLineHelper.useHomeDir(previousHome.toString());
+    }
+
+    @Test
+    void theOutputOfAStartThatFailedIsInTheAnswerAndThenRemoved() throws 
Exception {
+        integration("echo 'ERROR: Failed to start route route1'; exit 1");
+
+        JsonObject result = run();
+
+        assertThat(result.getString("status")).isEqualTo("failed");
+        assertThat(result.getString("cause")).isEqualTo("Failed to start route 
route1");
+        assertThat(result.getString("output")).contains("ERROR: Failed to 
start route route1");
+        assertThat(logFiles()).isEmpty();
+    }
+
+    @Test
+    void theOutputOfAnIntegrationThatStartedIsRemovedWhenItEnds() throws 
Exception {
+        integration(RUNNING);
+
+        JsonObject result = run();
+
+        assertThat(result.getString("status")).isEqualTo("started");
+        long pid = result.getLong("pid");
+        Path output = camelDir.resolve(pid + "-run.log");
+        // while it runs, as for camel run --background
+        assertThat(output).content().contains("started");
+        assertThat(logFiles()).containsExactly(pid + "-run.log");
+
+        stop(pid);
+
+        await().atMost(Duration.ofSeconds(10)).until(() -> 
!Files.exists(output));
+    }
+
+    @Test
+    void aRestartRemovesTheOutputOfTheProcessThatStopped() throws Exception {
+        integration(RUNNING);
+        long pid = run().getLong("pid");
+
+        String answer = IntegrationLauncher.restart(pid);
+
+        assertThat(answer).startsWith("Restarted ");
+        long restarted = Long.parseLong(answer.substring(answer.lastIndexOf(' 
') + 1));
+        assertThat(camelDir.resolve(pid + "-run.log")).doesNotExist();
+        Path output = camelDir.resolve(restarted + "-run.log");
+        assertThat(output).exists();
+
+        stop(restarted);
+
+        await().atMost(Duration.ofSeconds(10)).until(() -> 
!Files.exists(output));
+    }
+
+    private JsonObject run() {
+        return IntegrationLauncher.run(project, List.of("orders.camel.yaml"), 
"orders", false, List.of());
+    }
+
+    // the arguments that camel run gets are the arguments of the script ($1, 
$2, ...), which it does not use
+    private void integration(String script) {
+        String body = script.replace("DIR", "'" + camelDir + "'");
+        IntegrationLauncher.camelCommand = () -> List.of("sh", "-c", body, 
"sh");
+    }
+
+    // as camel stop does
+    private void stop(long pid) throws IOException {
+        Files.deleteIfExists(camelDir.resolve(Long.toString(pid)));
+    }
+
+    private List<String> logFiles() throws IOException {
+        try (Stream<Path> files = Files.list(camelDir)) {
+            return files.map(f -> f.getFileName().toString()).filter(n -> 
n.endsWith(".log")).toList();
+        }
+    }
+}

Reply via email to