From 8248b19a170b17500e6c9f0e4f5d6a425bd1a074 Mon Sep 17 00:00:00 2001 From: Philemon Hilscher Date: Sat, 28 Jun 2025 18:46:38 +0200 Subject: [PATCH 1/2] Change the ansi support detection to return true for Windows 10+ Signed-off-by: Philemon Hilscher --- .../springframework/boot/ansi/AnsiOutput.java | 27 +++++++++-- .../boot/ansi/AnsiOutputTests.java | 45 +++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java b/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java index 22f7afffd107..8a06c59e3fdc 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java @@ -31,6 +31,7 @@ * * @author Phillip Webb * @author Yong-Hyun Kim + * @author Philemon Hilscher * @since 1.0.0 */ public abstract class AnsiOutput { @@ -43,8 +44,6 @@ public abstract class AnsiOutput { private static @Nullable Boolean ansiCapable; - private static final String OPERATING_SYSTEM_NAME = System.getProperty("os.name").toLowerCase(Locale.ENGLISH); - private static final String ENCODE_START = "\033["; private static final String ENCODE_END = "m"; @@ -171,13 +170,35 @@ private static boolean detectIfAnsiCapable() { } } } - return !(OPERATING_SYSTEM_NAME.contains("win")); + if (isWindows(System.getProperty("os.name"))) { + return isWindowsAnsiCapable(System.getProperty("os.version")); + } + return true; } catch (Throwable ex) { return false; } } + static boolean isWindows(String osName) { + return osName.toLowerCase(Locale.ENGLISH).contains("win"); + } + + static boolean isWindowsAnsiCapable(String osVersion) { + String[] parts = osVersion.split("\\."); + if (parts.length >= 2) { + try { + int major = Integer.parseInt(parts[0]); + int minor = Integer.parseInt(parts[1]); + // ANSI support on Windows 10 = 10.0, Build 10586+ + return (major > 10) || (major == 10 && minor >= 0); + } catch (NumberFormatException ex) { + return false; + } + } + return false; + } + /** * Possible values to pass to {@link AnsiOutput#setEnabled}. Determines when to output * ANSI escape sequences for coloring application output. diff --git a/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java b/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java index a89c3ece113d..652bbec800f9 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java @@ -16,18 +16,25 @@ package org.springframework.boot.ansi; +import java.util.stream.Stream; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.boot.ansi.AnsiOutput.Enabled; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * Tests for {@link AnsiOutput}. * * @author Phillip Webb + * @author Philemon Hilscher */ class AnsiOutputTests { @@ -48,4 +55,42 @@ void encoding() { assertThat(encoded).isEqualTo("ABDEF"); } + private static Stream provideOsNames() { + return Stream.of( + Arguments.of("", false), + Arguments.of("Windows 7", true), + Arguments.of("Windows 8", true), + Arguments.of("Windows 8.1", true), + Arguments.of("Windows 10", true), + Arguments.of("Windows 11", true), + Arguments.of("Linux", false), + Arguments.of("Mac OS X", false), + Arguments.of("Mac OS", false) + ); + } + + @ParameterizedTest + @MethodSource("provideOsNames") + void testDetectIfIsWindows(String osName, boolean expected) { + boolean actual = AnsiOutput.isWindows(osName); + assertEquals(expected, actual); + } + + private static Stream provideOsVersionNumbers() { + return Stream.of( + Arguments.of("", false), + Arguments.of("6.1", false), // Windows 7 / Server 2008 R2 + Arguments.of("6.2", false), // Windows 8 / Server 2012 + Arguments.of("6.3", false), // Windows 8.1 / Server 2012 R2 + Arguments.of("10.0", true) // Windows 10 / 11 / Server 2016+ + ); + } + + @ParameterizedTest + @MethodSource("provideOsVersionNumbers") + void testDetectIfIsWindowsAnsiCapable(String osVersion, boolean expected) { + boolean actual = AnsiOutput.isWindowsAnsiCapable(osVersion); + assertEquals(expected, actual); + } + } From 3b1349dcc746218a9fcb90a167cf02c7cd8f325e Mon Sep 17 00:00:00 2001 From: Philemon Hilscher Date: Mon, 23 Mar 2026 23:54:56 +0100 Subject: [PATCH 2/2] Adapt ansi support detection for Windows 11+ Signed-off-by: Philemon Hilscher --- .../springframework/boot/ansi/AnsiOutput.java | 41 +++++++---- .../boot/ansi/AnsiOutputTests.java | 69 ++++++++++++++++--- 2 files changed, 88 insertions(+), 22 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java b/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java index 8a06c59e3fdc..1c0cbc1ae241 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java @@ -17,8 +17,12 @@ package org.springframework.boot.ansi; import java.io.Console; +import java.io.IOException; import java.lang.reflect.Method; +import java.util.Arrays; import java.util.Locale; +import java.util.Optional; +import java.util.function.Supplier; import org.jspecify.annotations.Nullable; @@ -171,7 +175,8 @@ private static boolean detectIfAnsiCapable() { } } if (isWindows(System.getProperty("os.name"))) { - return isWindowsAnsiCapable(System.getProperty("os.version")); + Integer buildNumber = parseWindowsBuildNumber(WINDOWS_BUILD_NUMBER).orElse(0); + return isWindowsAnsiCapable(buildNumber); } return true; } @@ -184,19 +189,29 @@ static boolean isWindows(String osName) { return osName.toLowerCase(Locale.ENGLISH).contains("win"); } - static boolean isWindowsAnsiCapable(String osVersion) { - String[] parts = osVersion.split("\\."); - if (parts.length >= 2) { - try { - int major = Integer.parseInt(parts[0]); - int minor = Integer.parseInt(parts[1]); - // ANSI support on Windows 10 = 10.0, Build 10586+ - return (major > 10) || (major == 10 && minor >= 0); - } catch (NumberFormatException ex) { - return false; - } + private static final String CURRENT_BUILD = "CurrentBuild"; + private static final Supplier WINDOWS_BUILD_NUMBER = () -> { + try { + Process buildNumberRequest = Runtime.getRuntime().exec( + "reg query \"HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\" /v " + CURRENT_BUILD); + return new String(buildNumberRequest.getInputStream().readAllBytes()); + } catch (IOException e) { + return ""; } - return false; + }; + + static Optional parseWindowsBuildNumber(Supplier osBuildSupplier) { + String plainResult = osBuildSupplier.get(); + return Arrays.stream(plainResult.split("\\r\\n")) + .map(String::trim) + .filter(line -> line.startsWith(CURRENT_BUILD)) + .findFirst() + .map(s -> s.split("\\s+")[2]) + .map(Integer::decode); + } + + static boolean isWindowsAnsiCapable(Integer buildNumber) { + return buildNumber >= 22000; // 22,000+ -> Windows 11 or higher } /** diff --git a/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java b/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java index 652bbec800f9..5da1764a148b 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.ansi; +import java.util.Optional; import java.util.stream.Stream; import org.junit.jupiter.api.AfterAll; @@ -29,6 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests for {@link AnsiOutput}. @@ -76,21 +78,70 @@ void testDetectIfIsWindows(String osName, boolean expected) { assertEquals(expected, actual); } - private static Stream provideOsVersionNumbers() { + private static Stream provideOsBuildNumbers() { return Stream.of( - Arguments.of("", false), - Arguments.of("6.1", false), // Windows 7 / Server 2008 R2 - Arguments.of("6.2", false), // Windows 8 / Server 2012 - Arguments.of("6.3", false), // Windows 8.1 / Server 2012 R2 - Arguments.of("10.0", true) // Windows 10 / 11 / Server 2016+ + Arguments.of(7600, false), // Windows 7 / Server 2008 R2 + Arguments.of(9200, false), // Windows 8 / Server 2012 + Arguments.of(9600, false), // Windows 8.1 / Server 2012 R2 + Arguments.of(19045, false), // Windows 10 / Server 2016 - 2022 + Arguments.of(22000, true) // Windows 11 / Server 2025+ ); } @ParameterizedTest - @MethodSource("provideOsVersionNumbers") - void testDetectIfIsWindowsAnsiCapable(String osVersion, boolean expected) { - boolean actual = AnsiOutput.isWindowsAnsiCapable(osVersion); + @MethodSource("provideOsBuildNumbers") + void testDetectIfIsWindowsAnsiCapable(Integer osBuild, boolean expected) { + boolean actual = AnsiOutput.isWindowsAnsiCapable(osBuild); assertEquals(expected, actual); } + private static Stream provideRegistryResults() { + return Stream.of( + Arguments.of(""" + \r + HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\r + CurrentBuild REG_SZ 7601\r + \r + """, 7601), + Arguments.of(""" + \r + HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\r + CurrentBuild REG_SZ 9200\r + \r + """, 9200), + Arguments.of(""" + \r + HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\r + CurrentBuild REG_SZ 9600\r + \r + """, 9600), + Arguments.of(""" + \r + HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\r + CurrentBuild REG_SZ 19045\r + \r + """, 19045), + Arguments.of(""" + \r + HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\r + CurrentBuild REG_SZ 22000\r + \r + """, 22000), + Arguments.of(""" + \r + HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\r + CurrentBuild REG_DWORD 0x5855\r + \r + """, 22613) + ); + } + + @ParameterizedTest + @MethodSource("provideRegistryResults") + void testParseWindowsBuildNumber(String registryResult, Integer expected) { + Optional parsed = AnsiOutput.parseWindowsBuildNumber(() -> registryResult); + assertTrue(parsed.isPresent()); + assertEquals(expected, parsed.get()); + } + }