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>&lt;pid&gt;-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>&lt;procRoot&gt;/&lt;pid&gt;/cmdline</tt> instead of
      * <tt>/proc/&lt;pid&gt;/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)) {

Reply via email to