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 a897120ba2fc CAMEL-25449: camel-jbang - camel debug sends its commands 
in files of their own (#27955)
a897120ba2fc is described below

commit a897120ba2fc57b351b86b4c3d1a672ad879241e
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 16 18:37:31 2026 +0200

    CAMEL-25449: camel-jbang - camel debug sends its commands in files of their 
own (#27955)
    
    camel debug was the last command that always used the action file all the
    clients of an integration share (<pid>-action.json): a step, step over or
    skip over, and the attach of --remote-attach, could be overwritten by
    another client before the integration read it. It now sends them with
    ActionClient, which gives each command a file of its own (Camel 4.21 or
    later), uses the shared file only for an older integration, and waits until
    the integration took the command; a failed attach exits with its exit code.
    The monitor already did this.
    
    CamelCommand.getActionFile and getOutputFile, no longer used, are
    deprecated for plugin commands in favour of ActionClient.
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   7 ++
 .../dsl/jbang/core/commands/CamelCommand.java      |  15 +++
 .../camel/dsl/jbang/core/commands/Debug.java       |  51 ++++----
 .../dsl/jbang/core/commands/DebugCommandTest.java  | 138 +++++++++++++++++++++
 4 files changed, 189 insertions(+), 22 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 02549e8aedc7..f3aba301947a 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
@@ -491,6 +491,13 @@ Turn the option off with `=false`, such as 
`--maven-wrapper=false`, which works
   of the CLI (`3.27.3.1` came after `3.27.4`), and a `-SNAPSHOT` always comes 
before its release, as in Maven
   (`4.24.10-SNAPSHOT` came after `4.24.10`).
 
+=== camel-jbang - the shared action file is deprecated for plugin commands
+
+`CamelCommand.getActionFile` and `CamelCommand.getOutputFile`, the action and 
output files that all the clients of an
+integration share, are deprecated. A plugin command sends an action with 
`ActionClient` instead, which gives each action
+a file of its own, so that two clients cannot overwrite each other's action or 
answer, and uses the shared files only
+for an integration before Camel 4.21. `camel debug` sends its commands this 
way too.
+
 === camel-jpa, camel-hazelcast - an exchange marked rollback only is not 
consumed
 
 When a route marks the exchange rollback only (for example with 
`markRollbackOnly()` or `markRollbackOnlyLast()`)
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java
index 47a63cae7d74..ce97bafee5ef 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java
@@ -25,6 +25,7 @@ import java.util.Properties;
 import java.util.Stack;
 import java.util.concurrent.Callable;
 
+import org.apache.camel.dsl.jbang.core.common.ActionClient;
 import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.CliExitCode;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
@@ -188,10 +189,24 @@ public abstract class CamelCommand implements 
Callable<Integer> {
         return CommandLineHelper.getCamelDir().resolve(pid + "-status.json");
     }
 
+    /**
+     * The action file that all the clients of the integration share.
+     *
+     * @deprecated use {@link ActionClient}, which sends each action in a file 
of its own, and uses this file only for
+     *             an integration before Camel 4.21
+     */
+    @Deprecated(since = "4.24")
     public Path getActionFile(String pid) {
         return CommandLineHelper.getCamelDir().resolve(pid + "-action.json");
     }
 
+    /**
+     * The output file that all the clients of the integration share.
+     *
+     * @deprecated use {@link ActionClient}, which reads the answer to each 
action from a file of its own, and uses this
+     *             file only for an integration before Camel 4.21
+     */
+    @Deprecated(since = "4.24")
     public Path getOutputFile(String pid) {
         return CommandLineHelper.getCamelDir().resolve(pid + "-output.json");
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
index d03ef334d49a..2a9e37833fa9 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
@@ -39,6 +39,8 @@ import java.util.concurrent.atomic.AtomicReference;
 import java.util.stream.Collectors;
 
 import org.apache.camel.dsl.jbang.core.commands.action.MessageTableHelper;
+import org.apache.camel.dsl.jbang.core.common.ActionClient;
+import org.apache.camel.dsl.jbang.core.common.ActionClient.ActionResult;
 import org.apache.camel.dsl.jbang.core.common.CamelCommandHelper;
 import org.apache.camel.dsl.jbang.core.common.ChildProcess;
 import org.apache.camel.dsl.jbang.core.common.CliExitCode;
@@ -86,6 +88,9 @@ import static org.apache.camel.util.IOHelper.buffered;
                  "  camel debug hello.java --stop-on-exit=false" })
 public class Debug extends Run {
 
+    // how long a command to the debugger in the integration may wait until 
the integration took it
+    private static final long DEBUG_COMMAND_TIMEOUT = 5000;
+
     @CommandLine.Option(names = { "--remote-attach" },
                         description = "Attaches debugger remotely to an 
existing running Camel integration. (Add camel-cli-debug JAR to the existing 
Camel application and run before attaching this debugger)")
     boolean remoteAttach;
@@ -245,22 +250,23 @@ public class Debug extends Run {
         // the running integration to attach to (exactly one, or exit 3)
         long pid = ProcessFinder.requireOne(name).pid();
 
-        Path outputFile = getOutputFile(Long.toString(pid));
-        PathUtils.deleteFile(outputFile);
-
+        JsonObject root = new JsonObject();
+        root.put("action", "cli-debug");
+        root.put("command", "attach");
+        if (breakpoint != null) {
+            root.put("breakpoint", breakpoint);
+        }
+        ActionResult result;
         try {
-            JsonObject root = new JsonObject();
-            root.put("action", "cli-debug");
-            root.put("command", "attach");
-            if (breakpoint != null) {
-                root.put("breakpoint", breakpoint);
-            }
-            Path f = getActionFile(Long.toString(pid));
-            Files.writeString(f, root.toJson());
-        } catch (Exception e) {
+            result = ActionClient.send(pid, root, DEBUG_COMMAND_TIMEOUT);
+        } catch (IOException e) {
             printer().printErr("Cannot attach the debugger to PID " + pid + ": 
" + e.getMessage());
             return CliExitCode.FAILURE;
         }
+        if (!result.isOk()) {
+            printer().printErr("Cannot attach the debugger to PID " + pid + ": 
" + result.error());
+            return result.exitCode();
+        }
 
         // attach to this pid
         spawnPid = pid;
@@ -628,11 +634,14 @@ public class Debug extends Run {
         return 0;
     }
 
-    private void sendDebugCommand(long pid, String command, String breakpoint, 
int position) {
-        // ensure output file is deleted before executing action
-        Path outputFile = getOutputFile(Long.toString(pid));
-        PathUtils.deleteFile(outputFile);
-
+    /**
+     * Sends a command, such as step, to the debugger in the integration, and 
waits until the integration took it. The
+     * command goes in a file of its own (Camel 4.21 or later), so that 
another client of the integration cannot
+     * overwrite it before the integration read it.
+     *
+     * @return whether the integration took the command
+     */
+    static boolean sendDebugCommand(long pid, String command, String 
breakpoint, int position) {
         JsonObject root = new JsonObject();
         root.put("action", "debug");
         if (command != null) {
@@ -644,12 +653,10 @@ public class Debug extends Run {
         if (position > 0) {
             root.put("position", position);
         }
-        Path f = getActionFile(Long.toString(pid));
         try {
-            String text = root.toJson();
-            Files.writeString(f, text);
-        } catch (Exception e) {
-            // ignore
+            return ActionClient.send(pid, root, DEBUG_COMMAND_TIMEOUT).isOk();
+        } catch (IOException e) {
+            return false;
         }
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DebugCommandTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DebugCommandTest.java
new file mode 100644
index 000000000000..bc9e67c9079f
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DebugCommandTest.java
@@ -0,0 +1,138 @@
+/*
+ * 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.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Stream;
+
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+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 static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+/**
+ * camel debug sends its commands (step, step over, skip over) in a file of 
their own, which another client of the
+ * integration cannot overwrite, and the shared action file only to an 
integration before Camel 4.21.
+ */
+class DebugCommandTest {
+
+    private static final long PID = 4242;
+
+    @TempDir
+    Path home;
+
+    private Path previousHome;
+    private final AtomicBoolean running = new AtomicBoolean(true);
+    private final AtomicReference<String> requestFile = new 
AtomicReference<>();
+    private final AtomicReference<JsonObject> request = new 
AtomicReference<>();
+    private Thread integration;
+
+    @BeforeEach
+    void setup() throws Exception {
+        previousHome = CommandLineHelper.getHomeDir();
+        CommandLineHelper.useHomeDir(home.toString());
+        Files.createDirectories(CommandLineHelper.getCamelDir());
+    }
+
+    @AfterEach
+    void restore() throws Exception {
+        running.set(false);
+        if (integration != null) {
+            integration.join(TimeUnit.SECONDS.toMillis(10));
+        }
+        CommandLineHelper.useHomeDir(previousHome.toString());
+    }
+
+    @Test
+    void commandInAFileOfItsOwn() throws Exception {
+        integration("4.24.0", true);
+
+        assertThat(Debug.sendDebugCommand(PID, "stepover", null, 3)).isTrue();
+
+        assertThat(requestFile.get()).startsWith(PID + 
"-action-").endsWith(".json");
+        assertThat(request.get().getString("action")).isEqualTo("debug");
+        assertThat(request.get().getString("command")).isEqualTo("stepover");
+        assertThat(request.get().getInteger("position")).isEqualTo(3);
+        // the answer is removed
+        try (Stream<Path> files = Files.list(CommandLineHelper.getCamelDir())) 
{
+            assertThat(files.map(f -> f.getFileName().toString())).noneMatch(n 
-> n.contains("-output"));
+        }
+    }
+
+    @Test
+    void sharedFileOfAnIntegrationBefore421() throws Exception {
+        integration("4.20.0", false);
+
+        // the integration takes the command without confirming it
+        assertThat(Debug.sendDebugCommand(PID, "step", null, 0)).isTrue();
+
+        assertThat(requestFile.get()).isEqualTo(PID + "-action.json");
+        assertThat(request.get().getString("command")).isEqualTo("step");
+    }
+
+    /**
+     * A fake integration with the given version of Camel that takes the next 
action, and confirms it when asked to (as
+     * the connector does since Camel 4.24).
+     */
+    private void integration(String version, boolean confirms) throws 
IOException {
+        Files.writeString(CommandLineHelper.getCamelDir().resolve(PID + 
"-status.json"),
+                "{\"context\":{\"name\":\"orders\",\"version\":\"" + version + 
"\",\"phase\":5}}");
+        integration = new Thread(() -> {
+            Path dir = CommandLineHelper.getCamelDir();
+            await().pollInterval(Duration.ofMillis(10)).atMost(30, 
TimeUnit.SECONDS)
+                    .until(() -> !running.get() || next(dir).isPresent());
+            next(dir).ifPresent(f -> take(f, confirms));
+        }, "test-integration");
+        integration.setDaemon(true);
+        integration.start();
+    }
+
+    private static Optional<Path> next(Path dir) {
+        try (Stream<Path> files = Files.list(dir)) {
+            return files.filter(f -> f.getFileName().toString().startsWith(PID 
+ "-action")).findFirst();
+        } catch (IOException e) {
+            return Optional.empty();
+        }
+    }
+
+    private void take(Path file, boolean confirms) {
+        try {
+            request.set(Jsoner.deserialize(Files.readString(file), 
(JsonObject) null));
+            requestFile.set(file.getFileName().toString());
+            if (confirms) {
+                
Files.writeString(file.resolveSibling(file.getFileName().toString().replace("-action-",
 "-output-")),
+                        "{\"status\":\"success\"}");
+            }
+            Files.delete(file);
+        } catch (Exception e) {
+            // the command withdrew the request
+        }
+    }
+}

Reply via email to