From 52765777305021d0755de2da72126b623cb6a00e Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 22 May 2026 15:00:37 +0200 Subject: [PATCH] chore: Fix commands using System.console() to work inside camel shell Inside the JLine shell, System.console() returns null, causing subcommands that read interactive input to silently fail. This moves the active terminal reference to EnvironmentHelper so all commands can detect and use the shell's terminal for input/output. Commands fixed: - debug: user input (step, quit) now works inside shell - infra run: "press ENTER to stop" now works inside shell - init: interactive template picker now works inside shell - watch commands: "press enter" to stop now works inside shell - confirmOperation: interactive confirmation now works inside shell Co-Authored-By: Claude Opus 4.6 --- .../jbang/core/commands/CommandHelper.java | 12 +++-- .../camel/dsl/jbang/core/commands/Debug.java | 9 ++-- .../camel/dsl/jbang/core/commands/Init.java | 8 +++- .../camel/dsl/jbang/core/commands/Shell.java | 3 ++ .../jbang/core/commands/infra/InfraRun.java | 7 ++- .../jbang/core/common/EnvironmentHelper.java | 47 ++++++++++++++++++- 6 files changed, 69 insertions(+), 17 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CommandHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CommandHelper.java index 460ae9520cd1e..1bf2b16ad5e60 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CommandHelper.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CommandHelper.java @@ -17,6 +17,7 @@ package org.apache.camel.dsl.jbang.core.commands; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -26,6 +27,7 @@ import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper; import org.apache.camel.dsl.jbang.core.common.PathUtils; import org.apache.camel.dsl.jbang.core.common.Printer; +import org.jline.terminal.Terminal; public final class CommandHelper { @@ -93,8 +95,10 @@ public static boolean confirmOperation(String message, boolean yes) { System.out.print(message + " [y/N] "); System.out.flush(); try { - // Do not use try-with-resources here: closing the Scanner would close System.in - Scanner scanner = new Scanner(System.in); + Terminal terminal = EnvironmentHelper.getActiveTerminal(); + InputStream input = terminal != null ? terminal.input() : System.in; + // Do not use try-with-resources here: closing the Scanner would close the input stream + Scanner scanner = new Scanner(input); String answer = scanner.nextLine().trim().toLowerCase(); return "y".equals(answer) || "yes".equals(answer); } catch (Exception e) { @@ -116,8 +120,8 @@ public ReadConsoleTask(Runnable listener) { @Override public void run() { - if (System.console() != null) { - System.console().readLine(); + String line = EnvironmentHelper.readLine(); + if (line != null) { listener.run(); } } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java index c1b1b56bc8871..67812323163ab 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java @@ -17,7 +17,6 @@ package org.apache.camel.dsl.jbang.core.commands; import java.io.BufferedReader; -import java.io.Console; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -41,6 +40,7 @@ import org.apache.camel.dsl.jbang.core.commands.action.MessageTableHelper; import org.apache.camel.dsl.jbang.core.common.CamelCommandHelper; import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; +import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper; import org.apache.camel.dsl.jbang.core.common.PathUtils; import org.apache.camel.dsl.jbang.core.common.ProcessHelper; import org.apache.camel.dsl.jbang.core.common.VersionHelper; @@ -187,7 +187,6 @@ public Integer doCall() throws Exception { // read log input final AtomicBoolean quit = new AtomicBoolean(); - final Console c = System.console(); if (logLines > 0) { Thread t = new Thread(() -> { doReadLog(quit); @@ -196,7 +195,7 @@ public Integer doCall() throws Exception { } // read CLI input from user - Thread t2 = new Thread(() -> doRead(c, quit), "ReadCommand"); + Thread t2 = new Thread(() -> doRead(quit), "ReadCommand"); t2.start(); do { @@ -285,9 +284,9 @@ private void doReadLog(AtomicBoolean quit) { } while (!quit.get()); } - private void doRead(Console c, AtomicBoolean quit) { + private void doRead(AtomicBoolean quit) { do { - String line = c.readLine(); + String line = EnvironmentHelper.readLine(); if (line != null) { line = line.trim(); if ("q".equalsIgnoreCase(line) || "quit".equalsIgnoreCase(line) || "exit".equalsIgnoreCase(line)) { diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java index 00772ae1cfa47..da2d911daf109 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java @@ -34,6 +34,7 @@ import org.apache.camel.CamelContext; import org.apache.camel.dsl.jbang.core.commands.catalog.KameletCatalogHelper; import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; +import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper; import org.apache.camel.dsl.jbang.core.common.ResourceDoesNotExist; import org.apache.camel.dsl.jbang.core.common.VersionHelper; import org.apache.camel.github.GistResourceResolver; @@ -43,6 +44,7 @@ import org.apache.camel.util.FileUtil; import org.apache.camel.util.IOHelper; import org.apache.commons.io.IOUtils; +import org.jline.terminal.Terminal; import picocli.CommandLine.Command; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; @@ -106,7 +108,7 @@ public Integer doCall() throws Exception { } if (file == null) { // try interactive picker if running in a TTY and not in CI - if (System.console() != null && System.getenv("CI") == null) { + if (EnvironmentHelper.isInteractiveTerminal()) { return interactivePicker(); } printer().printErr("Missing required parameter: "); @@ -309,7 +311,9 @@ private int interactivePicker() throws Exception { pipeTemplates.add(new String[] { "init-pipe.yaml", "Pipe CR (source to sink)", ".yaml" }); categories.put("Pipes and CRs", pipeTemplates); - Scanner scanner = new Scanner(System.in); + Terminal activeTerminal = EnvironmentHelper.getActiveTerminal(); + InputStream scannerInput = activeTerminal != null ? activeTerminal.input() : System.in; + Scanner scanner = new Scanner(scannerInput); // Step 1: Pick a category printer().println("Select a template category:"); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java index da04270700771..7f406b61a6b9e 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java @@ -99,8 +99,11 @@ public Integer doCall() throws Exception { } try (org.jline.shell.Shell shell = builder.build()) { + EnvironmentHelper.setActiveTerminal(shell.terminal()); printBanner(shell, camelVersion, colorEnabled); shell.run(); + } finally { + EnvironmentHelper.setActiveTerminal(null); } return 0; } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRun.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRun.java index 4e1395de3fec2..7aed979d3784d 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRun.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRun.java @@ -16,7 +16,6 @@ */ package org.apache.camel.dsl.jbang.core.commands.infra; -import java.io.Console; import java.io.File; import java.io.IOException; import java.lang.reflect.Method; @@ -32,6 +31,7 @@ import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; +import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper; import org.apache.camel.dsl.jbang.core.common.Printer; import org.apache.camel.dsl.jbang.core.common.RuntimeUtil; import org.apache.camel.main.download.DependencyDownloaderClassLoader; @@ -226,15 +226,14 @@ protected Integer doRun(String testService, String testServiceImplementation, Te final CountDownLatch latch = new CountDownLatch(1); // running in foreground then wait for user to exit - final Console c = System.console(); - if (c != null) { + if (EnvironmentHelper.isInteractiveTerminal()) { if (!jsonOutput) { printer().println("Press ENTER to stop the execution"); } Thread t = new Thread(() -> { boolean quit = false; do { - String line = c.readLine(); + String line = EnvironmentHelper.readLine(); if (line != null) { quit = true; latch.countDown(); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EnvironmentHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EnvironmentHelper.java index e35385be27c21..034c59ba1914d 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EnvironmentHelper.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EnvironmentHelper.java @@ -16,6 +16,13 @@ */ package org.apache.camel.dsl.jbang.core.common; +import java.io.BufferedReader; +import java.io.Console; +import java.io.IOException; +import java.io.InputStreamReader; + +import org.jline.terminal.Terminal; + /** * Helper for detecting environment characteristics such as CI environments, color support, and interactive terminals. * @@ -32,9 +39,45 @@ */ public final class EnvironmentHelper { + private static volatile Terminal activeTerminal; + private EnvironmentHelper() { } + /** + * Sets the active JLine terminal. Called by the shell command to make the terminal available to subcommands. + */ + public static void setActiveTerminal(Terminal terminal) { + activeTerminal = terminal; + } + + /** + * Returns the active JLine terminal, or null if not running inside the shell. + */ + public static Terminal getActiveTerminal() { + return activeTerminal; + } + + /** + * Reads a single line from the best available input source: the active JLine terminal if inside the shell, + * otherwise {@link System#console()}. + * + * @return the line read, or null if no input source is available or an error occurs + */ + public static String readLine() { + Terminal terminal = activeTerminal; + if (terminal != null) { + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(terminal.input())); + return reader.readLine(); + } catch (IOException e) { + return null; + } + } + Console c = System.console(); + return c != null ? c.readLine() : null; + } + /** * Determines whether colored output should be enabled based on environment variables and terminal capabilities. * @@ -59,7 +102,7 @@ public static boolean isColorEnabled() { if (getEnv("FORCE_COLOR") != null) { return true; } - return System.console() != null; + return activeTerminal != null || System.console() != null; } /** @@ -80,7 +123,7 @@ public static boolean isCIEnvironment() { * @return true if the terminal supports interactive prompts */ public static boolean isInteractiveTerminal() { - return System.console() != null && !isCIEnvironment(); + return (activeTerminal != null || System.console() != null) && !isCIEnvironment(); } // Visible for testing - allows overriding in tests