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 92464a92aecf CAMEL-26911: camel-jbang - camel stop must not signal a
process that reused the pid of a gone integration (#29811)
92464a92aecf is described below
commit 92464a92aecf377ec1ab83028778f5433f194a97
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Nov 13 11:02:42 2026 +0100
CAMEL-26911: camel-jbang - camel stop must not signal a process that reused
the pid of a gone integration (#29811)
The CLI treated any live pid with a ~/.camel/<pid>-status.json as a Camel
integration. An integration that is killed (kill -9, camel stop --kill, the OOM
killer) or crashes cannot remove its files, so once the OS reused its pid,
camel ps listed a ghost and camel stop sent SIGTERM, or SIGKILL with --kill, to
the unrelated process.
ProcessHelper.isStale(ProcessHandle, Path): a status file last written
before the process with that pid started cannot be its own, so the pid is
skipped. It compares the start instant of the process from the OS with the
modification time of the file (10 seconds of slack), so it also works with
integrations of older Camel versions. When either time is unknown, the
behaviour is as before. Used by ProcessFinder (camel ps, stop, restart, log,
trace, receive, cmd, and the AI and MCP tools [...]
Tests: ProcessHelperTest.aFileWrittenBeforeItsProcessStartedIsStale and
ProcessFinderTest.aStatusFileOlderThanItsProcessIsLeftOverAndFindsNothing.
This is the first item of CAMEL-26911; the other items follow in separate
PRs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../camel/dsl/jbang/core/common/ProcessFinder.java | 7 ++++-
.../camel/dsl/jbang/core/common/ProcessHelper.java | 32 ++++++++++++++++++++++
.../dsl/jbang/core/common/ProcessFinderTest.java | 27 ++++++++++++++++++
.../dsl/jbang/core/common/ProcessHelperTest.java | 32 ++++++++++++++++++++++
.../dsl/jbang/core/commands/tui/TuiHelper.java | 9 ++++--
5 files changed, 104 insertions(+), 3 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ProcessFinder.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ProcessFinder.java
index e2654d90dd72..cd396fe83ae2 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ProcessFinder.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ProcessFinder.java
@@ -207,7 +207,12 @@ public final class ProcessFinder {
if (byPid ? ph.pid() != pid : skipThisProcess && ph.pid()
== current) {
return;
}
- JsonObject status = readStatus(statusFiles.get(ph.pid()));
+ Path file = statusFiles.get(ph.pid());
+ if (ProcessHelper.isStale(ph, file)) {
+ // left over from an integration that is gone, and the
pid is now another process
+ return;
+ }
+ JsonObject status = readStatus(file);
if (status == null || stopped(status)) {
return;
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ProcessHelper.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ProcessHelper.java
index c5ea0c2915cf..9e65884ac559 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ProcessHelper.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ProcessHelper.java
@@ -20,6 +20,7 @@ import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -47,9 +48,40 @@ public final class ProcessHelper {
/** Where the command line of a process is read on Linux. */
private static volatile Path procRoot = Path.of("/proc");
+ /**
+ * How much older than its process the file of an integration may look and
still be its own: the start time of a
+ * process is not exact on every OS, and the file system may have a clock
of its own.
+ */
+ private static final long STALE_FILE_SLACK_MILLIS = 10000;
+
private ProcessHelper() {
}
+ /**
+ * Whether a file of an integration in <tt>~/.camel</tt>, such as
<tt><pid>-status.json</tt>, was last written
+ * before the process with that pid started. The file is then left over
from an integration that is gone, killed or
+ * crashed before it could remove its files, and the pid now belongs to
another process, which must not be taken for
+ * that integration (CAMEL-26911).
+ *
+ * @param ph the process with the pid of the file
+ * @param file the file
+ * @return true when the file is older than the process; false when
it is not, or when the start of the process
+ * or the time of the file is not known
+ * @since 4.24
+ */
+ public static boolean isStale(ProcessHandle ph, Path file) {
+ Optional<Instant> start = ph.info().startInstant();
+ if (start.isEmpty()) {
+ return false;
+ }
+ try {
+ long written = Files.getLastModifiedTime(file).toMillis();
+ return written < start.get().toEpochMilli() -
STALE_FILE_SLACK_MILLIS;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
/**
* Reads the command line of a process from
<tt><procRoot>/<pid>/cmdline</tt> instead of
* <tt>/proc/<pid>/cmdline</tt>. Unit tests may set this to give a
process the command line they need.
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ProcessFinderTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ProcessFinderTest.java
index 6aed63614207..62372909aad1 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ProcessFinderTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ProcessFinderTest.java
@@ -18,6 +18,9 @@ package org.apache.camel.dsl.jbang.core.common;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.time.Duration;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -84,6 +87,25 @@ class ProcessFinderTest {
assertThat(ProcessFinder.find("*")).extracting(CamelProcess::pid).containsExactly(100L,
200L);
}
+ @Test
+ void aStatusFileOlderThanItsProcessIsLeftOverAndFindsNothing() throws
Exception {
+ Instant start = Instant.now().minus(Duration.ofMinutes(5));
+ // a status file left behind by an integration that was killed, and
its pid is now another process
+ status(100, "orders", 5);
+
Files.setLastModifiedTime(CommandLineHelper.getCamelDir().resolve("100-status.json"),
+ FileTime.from(start.minus(Duration.ofHours(1))));
+ processes.add(handle(100, start));
+ // a running integration writes its status after it started
+ status(200, "billing", 5);
+ processes.add(handle(200, start));
+
+ // camel stop must not signal the process that now has the pid
+
assertThat(ProcessFinder.findAll()).extracting(CamelProcess::pid).containsExactly(200L);
+ assertThat(ProcessFinder.find("orders")).isEmpty();
+ assertThat(ProcessFinder.find("100")).isEmpty();
+
assertThat(ProcessFinder.find("billing")).extracting(CamelProcess::pid).containsExactly(200L);
+ }
+
@Test
void nameIsAPrefixUnlessItEndsWithExclamationMark() throws Exception {
running(100, "orders", 5);
@@ -197,11 +219,16 @@ class ProcessFinderTest {
}
private static ProcessHandle handle(long pid) {
+ return handle(pid, null);
+ }
+
+ private static ProcessHandle handle(long pid, Instant start) {
ProcessHandle ph = mock(ProcessHandle.class);
ProcessHandle.Info info = mock(ProcessHandle.Info.class);
when(ph.pid()).thenReturn(pid);
when(ph.info()).thenReturn(info);
when(info.commandLine()).thenReturn(Optional.empty());
+ when(info.startInstant()).thenReturn(Optional.ofNullable(start));
return ph;
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ProcessHelperTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ProcessHelperTest.java
index e5da42955086..8371321e55e3 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ProcessHelperTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ProcessHelperTest.java
@@ -19,7 +19,9 @@ package org.apache.camel.dsl.jbang.core.common;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
import java.time.Duration;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -247,6 +249,28 @@ class ProcessHelperTest {
return String.join(":", jars);
}
+ @Test
+ void aFileWrittenBeforeItsProcessStartedIsStale() throws Exception {
+ Instant start = Instant.parse("2026-11-13T08:00:00Z");
+ Path file = proc.resolve("100-status.json");
+ Files.writeString(file, "{}");
+
+ // left over from an integration that is gone: the pid is now another
process
+ Files.setLastModifiedTime(file,
FileTime.from(start.minus(Duration.ofHours(1))));
+ assertThat(ProcessHelper.isStale(started(start), file)).isTrue();
+ // the start of a process is not exact on every OS, and a file system
may have a clock of its own
+ Files.setLastModifiedTime(file, FileTime.from(start.minusSeconds(5)));
+ assertThat(ProcessHelper.isStale(started(start), file)).isFalse();
+ // written by the process
+ Files.setLastModifiedTime(file, FileTime.from(start.plusSeconds(30)));
+ assertThat(ProcessHelper.isStale(started(start), file)).isFalse();
+
+ // not known
+ Files.setLastModifiedTime(file,
FileTime.from(start.minus(Duration.ofHours(1))));
+ assertThat(ProcessHelper.isStale(started(null), file)).isFalse();
+ assertThat(ProcessHelper.isStale(started(start),
proc.resolve("200-status.json"))).isFalse();
+ }
+
private static JsonObject root(String contextName) {
JsonObject root = new JsonObject();
root.put("context", new JsonObject(Map.of("name", contextName)));
@@ -262,4 +286,12 @@ class ProcessHelperTest {
when(ph.children()).thenAnswer(inv -> Stream.empty());
return ph;
}
+
+ private static ProcessHandle started(Instant start) {
+ ProcessHandle.Info info = mock(ProcessHandle.Info.class);
+ when(info.startInstant()).thenReturn(Optional.ofNullable(start));
+ ProcessHandle ph = mock(ProcessHandle.class);
+ when(ph.info()).thenReturn(info);
+ return ph;
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
index a28c3ec63113..dd1d6d806fe5 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
@@ -75,12 +75,17 @@ final class TuiHelper {
continue;
}
// check process is still alive
- if
(!ProcessHandle.of(pid).map(ProcessHandle::isAlive).orElse(false)) {
+ ProcessHandle ph =
ProcessHandle.of(pid).filter(ProcessHandle::isAlive).orElse(null);
+ if (ph == null) {
+ continue;
+ }
+ Path file = statusFileResolver.apply(Long.toString(pid));
+ if (file != null && ProcessHelper.isStale(ph, file)) {
+ // left over from an integration that is gone, and the pid is
now another process
continue;
}
JsonObject root = loadStatus(pid, statusFileResolver);
if (root != null) {
- ProcessHandle ph = ProcessHandle.of(pid).orElse(null);
String pName = ProcessHelper.extractName(root, ph);
pName = FileUtil.onlyName(pName);
if (pName != null && !pName.isEmpty() &&
PatternHelper.matchPattern(pName, pat)) {