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();
+ }
+}