This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26911-stop-pid in repository https://gitbox.apache.org/repos/asf/camel.git
commit 93369e31497c47abc172e0bff21523b4aeaf6c95 Author: Claus Ibsen <[email protected]> AuthorDate: Fri Nov 13 09:46:09 2026 +0100 CAMEL-26911: camel-jbang - camel stop must not signal a process that reused the pid of a gone integration A status file in ~/.camel that was last written before the process with that pid started is left over from an integration that was killed or crashed, so camel ps, camel stop and the TUI no longer take that process for it. 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)) {
