This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch quick-fix/run-and-quit in repository https://gitbox.apache.org/repos/asf/camel.git
commit 90107b56793ada8ce3419c47740f5721fcb0d9a7 Author: Claus Ibsen <[email protected]> AuthorDate: Thu Oct 1 17:11:54 2026 +0200 chore: camel-jbang - the run form warns about a taken port, and quitting says which integrations started here keep running Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../ROOT/pages/camel-jbang-tui-settings.adoc | 2 +- .../modules/ROOT/pages/camel-jbang-tui.adoc | 3 +- .../dsl/jbang/core/commands/tui/ActionsPopup.java | 1 + .../dsl/jbang/core/commands/tui/CamelMonitor.java | 20 +++++- .../dsl/jbang/core/commands/tui/DialogHelper.java | 38 ++++++++++ .../dsl/jbang/core/commands/tui/LaunchManager.java | 18 +++++ .../dsl/jbang/core/commands/tui/PopupManager.java | 42 ++++++++++- .../dsl/jbang/core/commands/tui/PortCheck.java | 83 ++++++++++++++++++++++ .../jbang/core/commands/tui/RunOptionsForm.java | 41 ++++++++++- .../jbang/core/commands/tui/DialogHelperTest.java | 17 +++++ .../jbang/core/commands/tui/PopupManagerTest.java | 23 ++++++ .../dsl/jbang/core/commands/tui/PortCheckTest.java | 62 ++++++++++++++++ 12 files changed, 344 insertions(+), 6 deletions(-) diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc index 96e3ab5bcba1..85e17f2d1bc5 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc @@ -35,7 +35,7 @@ Press *F2* to open the actions menu with quick access to common operations: | Settings... | Change the theme, the starting tab, and the default run-from-folder. | Take Screenshot | Export the current screen as SVG, text, or ANSI art. | Start/Stop Tape Recording | Record your session as a `.tape` file for demos. -| Quit | Quit the TUI, with the same confirmation as pressing *q*. The last entry of the menu, so quitting is one menu away on tabs and in input fields where *q* is taken by something else. +| Quit | Quit the TUI, with the same confirmation as pressing *q*. The last entry of the menu, so quitting is one menu away on tabs and in input fields where *q* is taken by something else. Integrations started from the TUI keep running after it quits (until the terminal closes); when some do, the confirmation says how many, and *s* stops them and quits. |=== === Sending Test Messages diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc index 71a2c91b5205..c124ccdd6e76 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc @@ -68,7 +68,8 @@ Spring Boot, Quarkus, or JBang. This makes it easy to try any example on all thr changing a single line of code. The first three run the example in a separate JVM that only contains the dependencies of the example (like a production deployment), while JBang runs it in-process in the Camel CLI JVM, which starts faster but has the CLI on the classpath as well. You can also set the integration name, toggle dev mode, and -add extra dependencies. +add extra dependencies. When the port the app will listen on (the one you set, else 8080) is taken already, the form +says by whom, so a second app with an HTTP server does not fail to start. The example starts running in the background. The TUI auto-selects it as soon as it appears. From there you can explore tabs, watch messages flow, inspect the route diagram, and experiment. 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 9b0884f339cb..e648baf0beba 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 @@ -144,6 +144,7 @@ class ActionsPopup { Runnable burstCallback, Set<String> stoppingPids) { this.runningNames = runningNames; this.integrations = integrations; + runOptionsForm.setIntegrations(integrations); this.infraServices = infraServices; this.captionOverlay = captionOverlay; this.screenshotAction = screenshotAction; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java index 32fec82f9560..57f648a60b54 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java @@ -533,7 +533,21 @@ public class CamelMonitor extends CamelCommand { */ void quitTui(boolean confirm) { if (confirm && ctx.confirmActions) { - popupManager.showConfirm("Confirm Quit", " Quit the TUI? ", () -> runner.quit()); + LaunchManager launches = actionsPopup.getLaunchManager(); + long running = launches.runningLaunchCount(); + if (running > 0) { + // what was started from here keeps running when the TUI quits: say so, and offer to stop it + String what = running == 1 + ? "1 integration started here keeps running" + : running + " integrations started here keep running"; + popupManager.showConfirm("Confirm Quit", " Quit the TUI? ", what, () -> runner.quit(), + 's', running == 1 ? "stop it and quit" : "stop them and quit", () -> { + launches.stopLaunched(); + runner.quit(); + }); + } else { + popupManager.showConfirm("Confirm Quit", " Quit the TUI? ", () -> runner.quit()); + } } else { runner.quit(); } @@ -2772,6 +2786,10 @@ public class CamelMonitor extends CamelCommand { filesBrowser.renderFooter(spans); } else if (popupManager.isKillConfirmVisible() || popupManager.isConfirmVisible()) { hint(spans, "Enter", "confirm"); + String extra = popupManager.isConfirmVisible() ? popupManager.confirmExtraHint() : null; + if (extra != null) { + hint(spans, extra.substring(0, 1), extra.substring(2)); + } hintLast(spans, "Esc", "cancel"); } else if (popupManager.isSwitchPopupVisible()) { hint(spans, "Enter", "switch"); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java index afd8acd1d076..d645a2ac92a7 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java @@ -16,6 +16,9 @@ */ package org.apache.camel.dsl.jbang.core.commands.tui; +import java.util.ArrayList; +import java.util.List; + import dev.tamboui.layout.Rect; import dev.tamboui.style.Style; import dev.tamboui.terminal.Frame; @@ -127,6 +130,41 @@ final class DialogHelper { return popup; } + /** + * Renders a confirm dialog with a line that tells more and, when {@code extraKey} is given, a second choice on its + * own key between Enter and Esc. + */ + static Rect renderConfirm( + Frame frame, Rect area, String title, String message, String detail, String extraKey, String extraLabel) { + Style accent = Theme.warning(); + String titleText = " " + title + " "; + String msg = message.trim(); + Line hints = extraKey != null + ? TuiHelper.hintLine("Enter", "confirm", extraKey, extraLabel, "Esc", "cancel") + : TuiHelper.hintLine("Enter", "confirm", "Esc", "cancel"); + int contentW = Math.max(msg.length(), Math.max(detail != null ? detail.length() : 0, hints.width())); + int popupW = clampWidth(area, CONFIRM_MIN_WIDTH, Math.max(contentW + 6, titleText.length() + 4)); + Rect popup = centered(area, popupW, CONFIRM_HEIGHT + (detail != null ? 1 : 0)); + + frame.renderWidget(Clear.INSTANCE, popup); + Block block = Block.builder() + .borderType(BorderType.ROUNDED).borders(Borders.ALL) + .borderStyle(accent) + .title(Title.from(Line.from(Span.styled(titleText, accent.bold())))) + .build(); + frame.renderWidget(block, popup); + List<Line> lines = new ArrayList<>(); + lines.add(Line.empty()); + lines.add(Line.from(Span.styled(msg, accent.bold()))); + if (detail != null) { + lines.add(Line.from(Span.styled(detail, Theme.muted()))); + } + lines.add(Line.empty()); + lines.add(hints); + frame.renderWidget(Paragraph.builder().centered().text(Text.from(lines)).build(), block.inner(popup)); + return popup; + } + /** * Renders a dialog with a single text field, e.g. for entering a new file name. * diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java index c169ad862787..f5502aaf9e51 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java @@ -43,6 +43,8 @@ class LaunchManager { private final Supplier<List<InfraInfo>> infraServices; // added from the UI thread and from tool threads (MCP, the example launcher), read on the UI thread private final Queue<PendingLaunch> pendingLaunches = new ConcurrentLinkedQueue<>(); + // integrations started from the TUI (not infra services, which run in the background on their own) + private final Queue<Process> launched = new ConcurrentLinkedQueue<>(); private DeferredLaunch deferredLaunch; private volatile String pendingAutoSelect; private BiConsumer<String, Boolean> notificationCallback; @@ -161,6 +163,22 @@ class LaunchManager { void addPendingLaunch(String name, Process process, Path outputFile) { pendingLaunches.add(new PendingLaunch(name, process, outputFile, System.currentTimeMillis())); pendingAutoSelect = name; + launched.add(process); + } + + /** The integrations started from this TUI that still run: they keep running when the TUI quits. */ + long runningLaunchCount() { + launched.removeIf(p -> !p.isAlive()); + return launched.size(); + } + + /** Stops the integrations started from this TUI (the camel launcher and the JVM it started). */ + void stopLaunched() { + for (Process p : launched) { + p.descendants().forEach(ProcessHandle::destroy); + p.destroy(); + } + launched.clear(); } void addPendingLaunchNoAutoSelect(String name, Process process, Path outputFile) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java index 600bb1a35a24..544753f51a92 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java @@ -90,6 +90,10 @@ class PopupManager { private String confirmMessage; private Runnable confirmCallback; private Runnable cancelCallback; + private String confirmDetail; + private char confirmExtraKey; + private String confirmExtraLabel; + private Runnable confirmExtraCallback; // Last rendered popup rects for mouse hit-testing private Rect lastMorePopupRect; @@ -176,6 +180,11 @@ class PopupManager { return showKillConfirm; } + /** The second choice of the confirm dialog as "key label", or null when it has none. */ + String confirmExtraHint() { + return confirmExtraCallback != null ? confirmExtraKey + " " + confirmExtraLabel : null; + } + boolean isConfirmVisible() { return showConfirm; } @@ -245,9 +254,26 @@ class PopupManager { this.confirmMessage = message; this.confirmCallback = onConfirm; this.cancelCallback = onCancel; + this.confirmDetail = null; + this.confirmExtraLabel = null; + this.confirmExtraCallback = null; this.showConfirm = true; } + /** + * As {@link #showConfirm(String, String, Runnable)}, with a line that tells more, and a second choice on its own + * key (for example quit, or stop what was started and quit). + */ + void showConfirm( + String title, String message, String detail, Runnable onConfirm, char extraKey, String extraLabel, + Runnable onExtra) { + showConfirm(title, message, onConfirm, null); + this.confirmDetail = detail; + this.confirmExtraKey = extraKey; + this.confirmExtraLabel = extraLabel; + this.confirmExtraCallback = onExtra; + } + /** Closes the generic confirm dialog without running either callback (for example on a timeout). */ void dismissConfirm() { showConfirm = false; @@ -437,7 +463,14 @@ class PopupManager { } private boolean handleConfirmKeys(KeyEvent ke) { - if (ke.isConfirm()) { + if (confirmExtraCallback != null && ke.isChar(confirmExtraKey)) { + showConfirm = false; + Runnable cb = confirmExtraCallback; + confirmCallback = null; + cancelCallback = null; + confirmExtraCallback = null; + cb.run(); + } else if (ke.isConfirm()) { showConfirm = false; Runnable cb = confirmCallback; confirmCallback = null; @@ -724,7 +757,12 @@ class PopupManager { void renderConfirm(Frame frame, Rect area) { String msg = confirmMessage != null ? confirmMessage : ""; String title = confirmTitle != null ? confirmTitle : "Confirm"; - DialogHelper.renderConfirm(frame, area, title, msg, false); + if (confirmDetail != null || confirmExtraCallback != null) { + DialogHelper.renderConfirm(frame, area, title, msg, confirmDetail, + confirmExtraCallback != null ? String.valueOf(confirmExtraKey) : null, confirmExtraLabel); + } else { + DialogHelper.renderConfirm(frame, area, title, msg, false); + } } int[] morePopupShortcut(KeyEvent ke) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PortCheck.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PortCheck.java new file mode 100644 index 000000000000..2cfbf7092757 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PortCheck.java @@ -0,0 +1,83 @@ +/* + * 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.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.URI; +import java.util.List; + +/** + * Whether the HTTP port of an app about to be run is taken already, and by whom: a Spring Boot app then fails with + * APPLICATION FAILED TO START, and a Camel app cannot start its HTTP server. + */ +final class PortCheck { + + /** The port an app listens on when none is given (Camel, Spring Boot and Quarkus alike). */ + static final int DEFAULT_PORT = 8080; + + private PortCheck() { + } + + /** Whether something listens on the port already. */ + static boolean inUse(int port) { + if (port <= 0) { + return false; + } + try (ServerSocket socket = new ServerSocket()) { + socket.setReuseAddress(false); + socket.bind(new InetSocketAddress(port)); + return false; + } catch (IOException e) { + return true; + } + } + + /** The integration that serves HTTP on the port, from what the TUI knows of the running ones; null if none. */ + static String owner(int port, List<IntegrationInfo> integrations) { + for (IntegrationInfo info : integrations) { + if (info.vanishing || info.phantom) { + continue; + } + for (HttpEndpointInfo ep : info.httpEndpoints) { + if (ep.url != null && portOf(ep.url) == port) { + return info.name != null ? info.name : info.pid; + } + } + } + return null; + } + + /** The warning for the run form, or null when the port is free. */ + static String warning(int port, List<IntegrationInfo> integrations) { + if (!inUse(port)) { + return null; + } + String owner = owner(port, integrations); + return "Port " + port + " is taken by " + (owner != null ? owner : "another process") + + "; set another if this app serves HTTP"; + } + + private static int portOf(String url) { + try { + return URI.create(url).getPort(); + } catch (IllegalArgumentException e) { + return -1; + } + } +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java index b463e69d6f4e..f2d3f3f35c71 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java @@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui; import java.util.ArrayList; import java.util.List; +import java.util.function.Supplier; import dev.tamboui.layout.Rect; import dev.tamboui.style.Style; @@ -65,6 +66,11 @@ class RunOptionsForm { private int page; private int selectedRow; private String errorMessage; + // the running integrations, to say who holds the port; the check is cached as binding a socket is not free + private Supplier<List<IntegrationInfo>> integrations = List::of; + private String checkedPort; + private long checkedAt; + private String portWarning; private static final String[] MAX_MODES = { "Max seconds:", "Max messages:", "Max idle secs:" }; private static final String[] MAX_FLAGS = { "--max-seconds=", "--max-messages=", "--max-idle-seconds=" }; @@ -170,6 +176,32 @@ class RunOptionsForm { this.errorMessage = error; } + void setIntegrations(Supplier<List<IntegrationInfo>> integrations) { + this.integrations = integrations; + } + + /** + * A warning when the port the app will listen on (the one given, else 8080) is taken already; null when it is free. + * Checked again every couple of seconds, or when the port changes. + */ + String portWarning(long now) { + String text = portInput != null ? portInput.text().trim() : ""; + if (text.equals(checkedPort) && now - checkedAt < 2000) { + return portWarning; + } + checkedPort = text; + checkedAt = now; + int port; + try { + port = text.isEmpty() ? PortCheck.DEFAULT_PORT : Integer.parseInt(text); + } catch (NumberFormatException e) { + portWarning = null; + return null; + } + portWarning = PortCheck.warning(port, integrations.get()); + return portWarning; + } + boolean isJaegerExport() { return otelAgent && otelExportTarget == 1; } @@ -506,7 +538,8 @@ class RunOptionsForm { private void renderOptionsPage(Frame frame, Rect area) { // wide enough for the runtime cycler to show all runtimes (Camel Main, Spring Boot, Quarkus, JBang) int popupW = Math.min(80, area.width() - 4); - int popupH = errorMessage != null ? PAGE1_HEIGHT + 1 : PAGE1_HEIGHT; + String warning = portWarning(System.currentTimeMillis()); + int popupH = PAGE1_HEIGHT + (errorMessage != null ? 1 : 0) + (warning != null ? 1 : 0); Rect popup = DialogHelper.centered(area, popupW, popupH); frame.renderWidget(Clear.INSTANCE, popup); @@ -606,6 +639,12 @@ class RunOptionsForm { renderCheckbox(frame, innerX, rowY, innerW, "Java Flight Recorder (JFR)", jfrEnabled, selectedRow == ROW_JFR); + if (warning != null) { + rowY++; + frame.renderWidget(Paragraph.from(Line.from( + Span.styled(TuiIcons.HEALTH_WARN + " " + warning, Theme.warning()))), + new Rect(innerX, rowY, innerW, 1)); + } if (errorMessage != null) { rowY++; Rect errorArea = new Rect(innerX, rowY, innerW, 1); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelperTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelperTest.java index c97c809c8899..19c1750ca8e6 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelperTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelperTest.java @@ -90,6 +90,23 @@ class DialogHelperTest { assertThat(rendered).doesNotContain("Enter"); } + @Test + void confirmDialogWithADetailAndASecondChoice() { + Buffer buffer = Buffer.empty(AREA); + Frame frame = Frame.forTesting(buffer); + + Rect popup = DialogHelper.renderConfirm(frame, AREA, "Confirm Quit", "Quit the TUI?", + "2 integrations started here keep running", "s", "stop them and quit"); + + String rendered = HealthTabRenderTest.bufferToString(buffer); + assertThat(rendered).contains("Quit the TUI?"); + assertThat(rendered).contains("2 integrations started here keep running"); + assertThat(rendered).contains(" Enter confirm"); + assertThat(rendered).contains(" s stop them and quit"); + assertThat(rendered).contains(" Esc cancel"); + assertThat(popup.height()).isEqualTo(DialogHelper.CONFIRM_HEIGHT + 1); + } + @Test void inputDialogShowsTitleAndPlaceholder() { Buffer buffer = Buffer.empty(AREA); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java index b648b287ea3a..216ce7911d1c 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java @@ -178,6 +178,29 @@ class PopupManagerTest { assertTrue(popupManager.isConfirmVisible(), "a stray key must not dismiss the confirm"); } + @Test + void confirmWithASecondChoice() { + // quitting while integrations started from the TUI run: quit, or stop them and quit + List<String> ran = new ArrayList<>(); + popupManager.showConfirm("Confirm Quit", " Quit the TUI? ", "1 integration started here keeps running", + () -> ran.add("quit"), 's', "stop it and quit", () -> ran.add("stop")); + assertEquals("s stop it and quit", popupManager.confirmExtraHint()); + + popupManager.handleKeyEvent(KeyEvent.ofChar('x'), 0, 2); + assertTrue(popupManager.isConfirmVisible()); + popupManager.handleKeyEvent(KeyEvent.ofChar('s'), 0, 2); + assertEquals(List.of("stop"), ran); + assertFalse(popupManager.isConfirmVisible()); + + // a plain confirm after it has no second choice left over + popupManager.showConfirm("Confirm Quit", " Quit? ", () -> ran.add("plain")); + assertEquals(null, popupManager.confirmExtraHint()); + popupManager.handleKeyEvent(KeyEvent.ofChar('s'), 0, 2); + assertEquals(List.of("stop"), ran); + popupManager.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE), 0, 2); + assertEquals(List.of("stop", "plain"), ran); + } + @Test void killConfirmIgnoresOtherKeysAndCancelsOnEscape() { popupManager.showKillConfirm(); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PortCheckTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PortCheckTest.java new file mode 100644 index 000000000000..8964cfd1df5b --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PortCheckTest.java @@ -0,0 +1,62 @@ +/* + * 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.net.InetSocketAddress; +import java.net.ServerSocket; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The run form warns when the port of the app is taken already, and names the integration that holds it. + */ +class PortCheckTest { + + @Test + void aTakenPortIsFoundWithItsOwner() throws Exception { + try (ServerSocket taken = new ServerSocket()) { + taken.bind(new InetSocketAddress(0)); + int port = taken.getLocalPort(); + assertThat(PortCheck.inUse(port)).isTrue(); + + IntegrationInfo holder = new IntegrationInfo(); + holder.pid = "42"; + holder.name = "circuit-breaker"; + HttpEndpointInfo ep = new HttpEndpointInfo(); + ep.url = "http://0.0.0.0:" + port + "/hello"; + holder.httpEndpoints.add(ep); + + assertThat(PortCheck.warning(port, List.of(holder))) + .isEqualTo("Port " + port + " is taken by circuit-breaker; set another if this app serves HTTP"); + assertThat(PortCheck.warning(port, List.of())).contains("taken by another process"); + } + } + + @Test + void aFreePortHasNoWarning() throws Exception { + int port; + try (ServerSocket probe = new ServerSocket(0)) { + port = probe.getLocalPort(); + } + assertThat(PortCheck.inUse(port)).isFalse(); + assertThat(PortCheck.warning(port, List.of())).isNull(); + assertThat(PortCheck.inUse(0)).isFalse(); + } +}
