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
+ }
+ }
+}