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 bee65d77b290 chore: camel-jbang - an integration that could not start 
on a port in use says which running integration has it (#29509)
bee65d77b290 is described below

commit bee65d77b290a682dd2b36dd5110cd351619435f
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 9 23:18:59 2026 +0100

    chore: camel-jbang - an integration that could not start on a port in use 
says which running integration has it (#29509)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |   2 +-
 .../jbang/core/commands/tui/DocViewerPopup.java    |  15 ++-
 .../dsl/jbang/core/commands/tui/PortInUse.java     | 122 +++++++++++++++++++++
 .../jbang/core/commands/tui/TuiToolRegistry.java   |   9 ++
 .../dsl/jbang/core/commands/tui/PortInUseTest.java | 104 ++++++++++++++++++
 5 files changed, 247 insertions(+), 5 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index b1e5710cfc84..70770520f41f 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -1419,7 +1419,7 @@ class ActionsPopup {
             setNotification("Failed: " + name + " (no output)", true);
             return;
         }
-        docViewerPopup.showFailureLog(name, logFile);
+        docViewerPopup.showFailureLog(name, logFile, ctx != null ? 
ctx.data.get() : List.of());
     }
 
     /**
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java
index 8183cdd16b8e..848094c2bd52 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java
@@ -553,16 +553,23 @@ class DocViewerPopup {
     }
 
     void showFailureLog(String name, Path logFile) {
+        showFailureLog(name, logFile, List.of());
+    }
+
+    /** The log of a run that failed, with a hint before it that says why, 
when the TUI can tell (a port in use). */
+    void showFailureLog(String name, Path logFile, List<IntegrationInfo> 
running) {
         List<String> logLines = TuiHelper.readAllLines(logFile);
         if (logLines.isEmpty()) {
             return;
         }
-        List<Line> lines = logLines.stream()
+        List<Line> hint = PortInUse.hint(logLines, running);
+        List<Line> lines = new ArrayList<>(hint);
+        logLines.stream()
                 .map(line -> TuiHelper.ansiToLine(line.replace("\t", "        
"), 0))
-                .collect(Collectors.toList());
+                .forEach(lines::add);
         openLines("Failed: " + name, lines);
-        // open at why it failed, which comes after a long startup log
-        docScroll = failureLine(logLines);
+        // open at why it failed, which comes after a long startup log; the 
hint says it at the top
+        docScroll = hint.isEmpty() ? failureLine(logLines) : 0;
     }
 
     /** The line to open a failure log at: a little above the last line that 
says why, else the top. */
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PortInUse.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PortInUse.java
new file mode 100644
index 000000000000..49c3fbeabddd
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PortInUse.java
@@ -0,0 +1,122 @@
+/*
+ * 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.tui;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+
+/**
+ * Why an integration did not start when its HTTP server could not bind its 
port, and who has it: two examples run from
+ * the TUI both serve on 8080, and the second fails with a stack trace that 
says "Address already in use" somewhere in
+ * its log. The hint names the running integration that serves HTTP on that 
port, and the way out.
+ */
+final class PortInUse {
+
+    // the host and port the server tried: "on 0.0.0.0:8080", 
"/127.0.0.1:8443", "localhost:9090"
+    private static final Pattern PORT
+            = 
Pattern.compile("(?:\\d{1,3}(?:\\.\\d{1,3}){3}|localhost|\\[[0-9a-fA-F:]*]|[\\w.-]+\\.\\w+):(\\d{2,5})\\b");
+    // the consumers that serve HTTP on the port of the integration
+    private static final Set<String> HTTP_SERVERS = Set.of("platform-http", 
"rest", "rest-openapi", "rest-api");
+    private static final int DEFAULT_PORT = 8080;
+
+    private PortInUse() {
+    }
+
+    /** The port the log says is in use, or -1 when it does not say so. */
+    static int port(List<String> logLines) {
+        for (String line : logLines) {
+            if (line.contains("Address already in use")) {
+                Matcher m = PORT.matcher(line);
+                if (m.find()) {
+                    return Integer.parseInt(m.group(1));
+                }
+            }
+        }
+        return -1;
+    }
+
+    /** The lines of the hint for the failure log of the integration, or none 
when it did not fail on a port. */
+    static List<Line> hint(List<String> logLines, List<IntegrationInfo> 
running) {
+        int port = port(logLines);
+        if (port < 0) {
+            return List.of();
+        }
+        List<Line> lines = new ArrayList<>();
+        IntegrationInfo owner = owner(port, running);
+        if (owner != null) {
+            lines.add(Line.from(Span.styled(" ✗ Port " + port + " is in use: 
", Theme.error().bold()),
+                    Span.raw("the running integration "), 
Span.styled(owner.name, Theme.label().bold()),
+                    Span.raw(" (pid " + owner.pid + ") serves HTTP on it.")));
+        } else {
+            lines.add(Line.from(Span.styled(" ✗ Port " + port + " is in use", 
Theme.error().bold()),
+                    Span.raw(" by another program.")));
+        }
+        // the next port, or the one before when it is the last port there is
+        int other = port < 65535 ? port + 1 : port - 1;
+        lines.add(Line.from(Span.raw("   Stop " + (owner != null ? "it" : 
"that program")
+                                     + " first, or give this integration 
another port: camel.server.port="
+                                     + other + " in its 
application.properties.")));
+        lines.add(Line.from(Span.raw("")));
+        return lines;
+    }
+
+    /** The running integration that serves HTTP on the port, as its 
application.properties configures it. */
+    static IntegrationInfo owner(int port, List<IntegrationInfo> running) {
+        for (IntegrationInfo info : running) {
+            if (info.phantom || info.vanishing || !servesHttp(info)) {
+                continue;
+            }
+            if (configuredPort(info.directory) == port) {
+                return info;
+            }
+        }
+        return null;
+    }
+
+    private static boolean servesHttp(IntegrationInfo info) {
+        return info.endpoints.stream().anyMatch(ep -> 
"in".equals(ep.direction) && HTTP_SERVERS.contains(ep.component));
+    }
+
+    /** camel.server.port of the application.properties in the directory, else 
the port Camel serves on by default. */
+    static int configuredPort(String directory) {
+        if (directory == null) {
+            return DEFAULT_PORT;
+        }
+        Path file = Path.of(directory, "application.properties");
+        if (!Files.isRegularFile(file)) {
+            return DEFAULT_PORT;
+        }
+        Properties p = new Properties();
+        try (Reader r = Files.newBufferedReader(file)) {
+            p.load(r);
+            return Integer.parseInt(p.getProperty("camel.server.port", 
String.valueOf(DEFAULT_PORT)).trim());
+        } catch (IOException | NumberFormatException e) {
+            return DEFAULT_PORT;
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
index 1d6d3f929bd4..c71ff3e96107 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
@@ -35,6 +35,8 @@ import dev.tamboui.buffer.Buffer;
 import dev.tamboui.export.ExportRequest;
 import dev.tamboui.style.Color;
 import dev.tamboui.style.Style;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.dsl.jbang.core.commands.ai.IntegrationSummary;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolConfirms;
@@ -1935,6 +1937,13 @@ class TuiToolRegistry {
             result.put("message", "Failed to start: " + name);
             if (outcome.log() != null) {
                 result.put("log", outcome.log());
+                // a port another integration has: which one, and the way out
+                List<Line> hint = 
PortInUse.hint(outcome.log().lines().toList(), facade.liveIntegrations());
+                if (!hint.isEmpty()) {
+                    result.put("hint", hint.stream()
+                            .map(l -> 
l.spans().stream().map(Span::content).collect(Collectors.joining()).strip())
+                            .filter(t -> 
!t.isEmpty()).collect(Collectors.joining(" ")));
+                }
             }
             // a tool error (MCP isError), which still has the log that tells 
why
             throw new ToolExecutionException(Jsoner.serialize(result));
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PortInUseTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PortInUseTest.java
new file mode 100644
index 000000000000..f22b3959cdd8
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PortInUseTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.tui;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Two examples run from the TUI both serve HTTP on 8080: the second fails, 
and the hint says which one has the port.
+ */
+class PortInUseTest {
+
+    private static final List<String> LOG = List.of(
+            "2026-11-09 18:02:45.265  INFO 482849 --- [main] 
AbstractCamelContext : Apache Camel (openapi-server) is starting",
+            "2026-11-09 18:02:45.684 ERROR 482849 --- [main] 
AbstractCamelContext : Error starting CamelContext"
+                                                                               
                                                 + " (openapi-server) due to 
exception thrown: Failed to start Vert.x HttpServer on 0.0.0.0:8080,"
+                                                                               
                                                 + " reason: 
java.net.BindException: Address already in use",
+            "Caused by: java.net.BindException: Address already in use");
+
+    @TempDir
+    Path dir;
+
+    @Test
+    void thePortTheLogSaysIsInUse() {
+        assertThat(PortInUse.port(LOG)).isEqualTo(8080);
+        assertThat(PortInUse.port(List.of("Error: Connection refused to 
localhost:8080"))).isEqualTo(-1);
+    }
+
+    @Test
+    void theHintNamesTheIntegrationThatServesOnThePort() throws Exception {
+        IntegrationInfo client = integration("http-client", "100", null);
+        // another one serves on 9090
+        Path other = Files.createDirectory(dir.resolve("other"));
+        Files.writeString(other.resolve("application.properties"), 
"camel.server.port = 9090\n");
+        IntegrationInfo api = integration("stock-api", "200", 
other.toString());
+
+        assertThat(PortInUse.owner(8080, List.of(api, 
client))).isSameAs(client);
+        assertThat(PortInUse.owner(9090, List.of(api, client))).isSameAs(api);
+        assertThat(text(PortInUse.hint(LOG, List.of(api, client))))
+                .startsWith(" ✗ Port 8080 is in use: the running integration 
http-client (pid 100) serves HTTP on it.")
+                .contains("camel.server.port=8081");
+    }
+
+    @Test
+    void anotherProgramHasThePort() {
+        // an integration without an HTTP server does not
+        IntegrationInfo timer = integration("timer-log", "300", null);
+        timer.endpoints.clear();
+
+        assertThat(text(PortInUse.hint(LOG, List.of(timer)))).startsWith(" ✗ 
Port 8080 is in use by another program.");
+        assertThat(PortInUse.hint(List.of("all good"), 
List.of(timer))).isEmpty();
+    }
+
+    @Test
+    void theLastPortSuggestsTheOneBefore() {
+        List<String> log = List.of("Failed to start Vert.x HttpServer on 
0.0.0.0:65535,"
+                                   + " reason: java.net.BindException: Address 
already in use");
+
+        assertThat(text(PortInUse.hint(log, 
List.of()))).contains("camel.server.port=65534 ");
+    }
+
+    private static IntegrationInfo integration(String name, String pid, String 
directory) {
+        IntegrationInfo info = new IntegrationInfo();
+        info.name = name;
+        info.pid = pid;
+        info.directory = directory;
+        EndpointInfo ep = new EndpointInfo();
+        ep.direction = "in";
+        ep.component = "platform-http";
+        info.endpoints.add(ep);
+        return info;
+    }
+
+    private static String text(List<Line> lines) {
+        StringBuilder sb = new StringBuilder();
+        for (Line l : lines) {
+            l.spans().stream().map(Span::content).forEach(sb::append);
+            sb.append('\n');
+        }
+        return sb.toString();
+    }
+}

Reply via email to