This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-23637 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 31ce7ba3016b458e375726f32d1612c72b786c42 Author: Claus Ibsen <[email protected]> AuthorDate: Fri May 29 09:31:56 2026 +0200 CAMEL-23615: camel-jbang - TUI auto-start infra services when running examples Co-Authored-By: Claude <[email protected]> --- .../camel/dsl/jbang/core/common/ExampleHelper.java | 9 ++ .../examples/camel-jbang-example-catalog.json | 29 ++++- .../dsl/jbang/core/commands/tui/ActionsPopup.java | 138 ++++++++++++++++----- 3 files changed, 144 insertions(+), 32 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java index faf401146db3..5e934d366d66 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java @@ -129,6 +129,15 @@ public final class ExampleHelper { return citrus != null && citrus; } + @SuppressWarnings("unchecked") + public static List<String> getInfraServices(JsonObject entry) { + Collection<String> services = (Collection<String>) entry.get("infraServices"); + if (services == null) { + return List.of(); + } + return new ArrayList<>(services); + } + @SuppressWarnings("unchecked") public static List<String> getFiles(JsonObject entry) { Collection<String> files = (Collection<String>) entry.get("files"); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json index d88e50b1dc0f..10c4a6e65692 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json @@ -17,6 +17,9 @@ "application.properties", "consumer.camel.yaml", "producer.camel.yaml" + ], + "infraServices": [ + "artemis" ] }, { @@ -120,6 +123,10 @@ "compose.yaml", "docling-langchain4j-rag.yaml", "sample.md" + ], + "infraServices": [ + "docling", + "ollama" ] }, { @@ -144,6 +151,10 @@ "examples/semiconductor-sector-analysis.pdf", "examples/tesla-q3-2024.pdf", "financial-doc-analyzer.yaml" + ], + "infraServices": [ + "docling", + "ollama" ] }, { @@ -165,6 +176,10 @@ "compose.yaml", "ftp.camel.yaml", "jbang.properties" + ], + "infraServices": [ + "artemis", + "ftp" ] }, { @@ -204,6 +219,9 @@ "README.md", "application.properties", "rest-api.camel.yaml" + ], + "infraServices": [ + "keycloak" ] }, { @@ -224,6 +242,9 @@ "README.md", "application.properties", "rest-api.camel.yaml" + ], + "infraServices": [ + "keycloak" ] }, { @@ -241,7 +262,7 @@ "hasCitrusTests": false, "files": [ "README.md", - "message-size.camel.yaml" + "orders.camel.yaml" ] }, { @@ -264,6 +285,9 @@ "infra/mosquitto.conf", "mqtt.camel.yaml", "start.sh" + ], + "infraServices": [ + "mosquitto" ] }, { @@ -435,6 +459,9 @@ "application.properties", "compose.yaml", "sql.camel.yaml" + ], + "infraServices": [ + "postgres" ] }, { 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 831f7937d8ad..f737a9af1e73 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 @@ -143,6 +143,7 @@ class ActionsPopup { private ScheduledExecutorService scheduler; private final List<PendingLaunch> pendingLaunches = new ArrayList<>(); + private DeferredExampleLaunch deferredLaunch; private String launchNotification; private boolean launchNotificationError; private long launchNotificationExpiry; @@ -651,6 +652,7 @@ class ActionsPopup { void tick(long now) { monitorPendingLaunches(now); + checkDeferredLaunch(now); if (launchNotification != null && now > launchNotificationExpiry) { launchNotification = null; } @@ -764,8 +766,10 @@ class ActionsPopup { boolean docker = ExampleHelper.requiresDocker(ex); boolean bundled = ExampleHelper.isBundled(ex); boolean citrus = ExampleHelper.hasCitrusTests(ex); + boolean infra = !ExampleHelper.getInfraServices(ex).isEmpty(); - String icons = (bundled ? "📦" : "🌐") + (docker ? "🐳" : " ") + (citrus ? "🍋" : " "); + String icons = (bundled ? "📦" : "🌐") + (docker ? "🐳" : " ") + + (infra ? "🔧" : " ") + (citrus ? "🍋" : " "); int nameCol = Math.min(30, width / 3); String padded = String.format("%-" + nameCol + "s", TuiHelper.truncate(name, nameCol)); String prefix = " " + icons + " " + padded + " "; @@ -786,7 +790,7 @@ class ActionsPopup { } } items.add(ListItem.from("")); - items.add(ListItem.from(" 📦 = bundled (offline) 🌐 = online (GitHub) 🐳 = Docker 🍋 = Citrus tests") + items.add(ListItem.from(" 📦 = bundled 🌐 = online 🐳 = Docker 🔧 = infra services 🍋 = Citrus tests") .style(Style.EMPTY.dim())); return items; } @@ -1160,28 +1164,18 @@ class ActionsPopup { } List<String> extraArgs = runOptionsForm.buildArgs(); runOptionsForm.close(); - try { - List<String> cmd = new ArrayList<>(LauncherHelper.getCamelCommand()); - cmd.add("run"); - cmd.add("--example=" + exampleName); - cmd.add("--logging-color=true"); - cmd.addAll(extraArgs); - Path outputFile = Files.createTempFile("camel-example-", ".log"); - outputFile.toFile().deleteOnExit(); - ProcessBuilder pb = new ProcessBuilder(cmd); - pb.redirectErrorStream(true); - pb.redirectOutput(outputFile.toFile()); - Process process = pb.start(); - pendingLaunches.add(new PendingLaunch(displayName, process, outputFile, System.currentTimeMillis())); - pendingAutoSelect = displayName; - launchNotification = "Starting: " + displayName; - launchNotificationError = false; - launchNotificationExpiry = System.currentTimeMillis() + 5000; - } catch (Exception e) { - launchNotification = "Failed to start: " + exampleName + " - " + e.getMessage(); - launchNotificationError = true; - launchNotificationExpiry = System.currentTimeMillis() + 10000; + + List<String> missing = findMissingInfraServices(selectedExample); + if (!missing.isEmpty()) { + if (!isContainerRuntimeAvailable()) { + setNotification("Docker/Podman required for infra services. Run Doctor for details", true); + return; + } + startMissingInfraAndDeferExample(missing, exampleName, displayName, extraArgs); + return; } + + doLaunchExample(exampleName, displayName, extraArgs); } // ---- Example Browser Navigation ---- @@ -1641,26 +1635,103 @@ class ActionsPopup { } String exampleName = example.getStringOrDefault("name", ""); showExampleBrowser = false; + + List<String> missing = findMissingInfraServices(example); + if (!missing.isEmpty()) { + if (!isContainerRuntimeAvailable()) { + setNotification("Docker/Podman required for infra services. Run Doctor for details", true); + return; + } + startMissingInfraAndDeferExample(missing, exampleName, exampleName, List.of()); + return; + } + + doLaunchExample(exampleName, exampleName, List.of()); + } + + private void doLaunchExample(String exampleName, String displayName, List<String> extraArgs) { try { List<String> cmd = new ArrayList<>(LauncherHelper.getCamelCommand()); cmd.add("run"); cmd.add("--example=" + exampleName); cmd.add("--logging-color=true"); + cmd.addAll(extraArgs); Path outputFile = Files.createTempFile("camel-example-", ".log"); outputFile.toFile().deleteOnExit(); ProcessBuilder pb = new ProcessBuilder(cmd); pb.redirectErrorStream(true); pb.redirectOutput(outputFile.toFile()); Process process = pb.start(); - pendingLaunches.add(new PendingLaunch(exampleName, process, outputFile, System.currentTimeMillis())); - pendingAutoSelect = exampleName; - launchNotification = "Starting: " + exampleName; - launchNotificationError = false; - launchNotificationExpiry = System.currentTimeMillis() + 5000; + pendingLaunches.add(new PendingLaunch(displayName, process, outputFile, System.currentTimeMillis())); + pendingAutoSelect = displayName; + setNotification("Starting: " + displayName, false); } catch (Exception e) { - launchNotification = "Failed to start: " + exampleName + " - " + e.getMessage(); - launchNotificationError = true; - launchNotificationExpiry = System.currentTimeMillis() + 10000; + setNotification("Failed to start: " + exampleName + " - " + e.getMessage(), true); + } + } + + private List<String> findMissingInfraServices(JsonObject example) { + List<String> required = ExampleHelper.getInfraServices(example); + if (required.isEmpty()) { + return List.of(); + } + Set<String> runningAliases = infraServices.get().stream() + .filter(i -> i.alive) + .map(i -> i.alias) + .collect(Collectors.toSet()); + List<String> missing = new ArrayList<>(); + for (String alias : required) { + if (!runningAliases.contains(alias)) { + missing.add(alias); + } + } + return missing; + } + + private void startMissingInfraAndDeferExample( + List<String> missingInfra, String exampleName, String displayName, List<String> extraArgs) { + for (String alias : missingInfra) { + try { + List<String> cmd = new ArrayList<>(LauncherHelper.getCamelCommand()); + cmd.add("infra"); + cmd.add("run"); + cmd.add(alias); + cmd.add("--background"); + Path outputFile = Files.createTempFile("camel-infra-", ".log"); + outputFile.toFile().deleteOnExit(); + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.redirectErrorStream(true); + pb.redirectOutput(outputFile.toFile()); + Process process = pb.start(); + pendingLaunches.add(new PendingLaunch(alias, process, outputFile, System.currentTimeMillis())); + } catch (Exception e) { + setNotification("Failed to start infra: " + alias + " - " + e.getMessage(), true); + return; + } + } + deferredLaunch = new DeferredExampleLaunch( + exampleName, displayName, extraArgs, missingInfra, System.currentTimeMillis()); + infraCatalog = null; + String infraList = String.join(", ", missingInfra); + setNotification("Starting infra: " + infraList + " → then: " + displayName, false); + } + + private void checkDeferredLaunch(long now) { + if (deferredLaunch == null) { + return; + } + Set<String> runningAliases = infraServices.get().stream() + .filter(i -> i.alive) + .map(i -> i.alias) + .collect(Collectors.toSet()); + boolean allReady = runningAliases.containsAll(deferredLaunch.requiredInfra); + if (allReady) { + DeferredExampleLaunch dl = deferredLaunch; + deferredLaunch = null; + doLaunchExample(dl.exampleName, dl.displayName, dl.extraArgs); + } else if (now - deferredLaunch.startTime > 120_000) { + deferredLaunch = null; + setNotification("Timeout waiting for infra services to start", true); } } @@ -1758,4 +1829,9 @@ class ActionsPopup { private record PendingLaunch(String name, Process process, Path outputFile, long startTime) { } + + private record DeferredExampleLaunch( + String exampleName, String displayName, List<String> extraArgs, + List<String> requiredInfra, long startTime) { + } }
