diff --git a/compat/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java b/compat/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java index ebcb335ac074..163954a7ed1c 100644 --- a/compat/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java +++ b/compat/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java @@ -532,6 +532,10 @@ void logging(CliRequest cliRequest) throws ExitException { || commandLine.hasOption(CLIManager.NON_INTERACTIVE)); if (isBatchMode || commandLine.hasOption(CLIManager.LOG_FILE)) { MessageUtils.setColorEnabled(false); + } else if (!isConsoleTerminal()) { + // On JDK 22+, System.console() returns non-null even when stdout is + // redirected to a pipe or file. Use Console.isTerminal() to detect this. + MessageUtils.setColorEnabled(false); } } @@ -628,6 +632,32 @@ void logging(CliRequest cliRequest) throws ExitException { } } + /** + * Checks whether the JVM console is connected to a real terminal. + * On JDK 22+, {@code Console.isTerminal()} returns {@code false} when stdout is redirected + * to a pipe or file, even though {@code System.console()} returns non-null. + * On older JDKs (before 22), falls back to checking {@code System.console() != null}, + * which is the pre-existing behavior. + */ + static boolean isConsoleTerminal() { + Console cons = System.console(); + if (cons == null) { + return false; + } + try { + // JDK 22+ provides Console.isTerminal() to distinguish a real terminal + // from a redirected console (pipe or file). + java.lang.reflect.Method isTerminal = cons.getClass().getMethod("isTerminal"); + return (Boolean) isTerminal.invoke(cons); + } catch (NoSuchMethodException e) { + // JDK < 22: System.console() != null was the best heuristic + return true; + } catch (ReflectiveOperationException e) { + // Unexpected reflection error; fall back to assuming terminal + return true; + } + } + private void version(CliRequest cliRequest) { if (cliRequest.verbose || cliRequest.commandLine.hasOption(CLIManager.SHOW_VERSION)) { System.out.println(CLIReportingUtils.showVersion()); diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java index 963d8b1706b7..d6b1adbe57cb 100644 --- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java @@ -360,8 +360,13 @@ protected final void doConfigureWithTerminal(C context, Terminal terminal) { // To align Maven with outcomes, we set here color enabled based on these premises. // Note: Maven3 suffers from similar thing: if you do `mvn3 foo > log.txt`, the output will // not be not colored (good), but Maven will print out "Message scheme: color". - MessageUtils.setColorEnabled( - context.coloredOutput != null ? context.coloredOutput : !Terminal.TYPE_DUMB.equals(terminal.getType())); + boolean colorEnabled; + if (context.coloredOutput != null) { + colorEnabled = context.coloredOutput; + } else { + colorEnabled = !Terminal.TYPE_DUMB.equals(terminal.getType()) && isConsoleTerminal(); + } + MessageUtils.setColorEnabled(colorEnabled); // handle rawStreams: some would like to act on true, some on false if (context.options().rawStreams().orElse(false)) { @@ -401,6 +406,32 @@ protected void doConfigureWithTerminalWithRawStreamsDisabled(C context) { // no need to set them back, this is already handled by MessageUtils.systemUninstall() above } + /** + * Checks whether the JVM console is connected to a real terminal. + * On JDK 22+, {@code Console.isTerminal()} returns {@code false} when stdout is redirected + * to a pipe or file, even though {@code System.console()} returns non-null. + * On older JDKs (before 22), falls back to checking {@code System.console() != null}, + * which is the pre-existing behavior. + */ + static boolean isConsoleTerminal() { + java.io.Console console = System.console(); + if (console == null) { + return false; + } + try { + // JDK 22+ provides Console.isTerminal() to distinguish a real terminal + // from a redirected console (pipe or file). + java.lang.reflect.Method isTerminal = console.getClass().getMethod("isTerminal"); + return (Boolean) isTerminal.invoke(console); + } catch (NoSuchMethodException e) { + // JDK < 22: System.console() != null was the best heuristic + return true; + } catch (ReflectiveOperationException e) { + // Unexpected reflection error; fall back to assuming terminal + return true; + } + } + protected Consumer determineWriter(C context) { if (context.writer == null) { context.writer = doDetermineWriter(context);