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..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; @@ -31,6 +35,7 @@ * * @author Phillip Webb * @author Yong-Hyun Kim + * @author Philemon Hilscher * @since 1.0.0 */ public abstract class AnsiOutput { @@ -43,8 +48,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 +174,46 @@ private static boolean detectIfAnsiCapable() { } } } - return !(OPERATING_SYSTEM_NAME.contains("win")); + if (isWindows(System.getProperty("os.name"))) { + Integer buildNumber = parseWindowsBuildNumber(WINDOWS_BUILD_NUMBER).orElse(0); + return isWindowsAnsiCapable(buildNumber); + } + return true; } catch (Throwable ex) { return false; } } + static boolean isWindows(String osName) { + return osName.toLowerCase(Locale.ENGLISH).contains("win"); + } + + 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 ""; + } + }; + + 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 + } + /** * 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..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,18 +16,27 @@ package org.springframework.boot.ansi; +import java.util.Optional; +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; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests for {@link AnsiOutput}. * * @author Phillip Webb + * @author Philemon Hilscher */ class AnsiOutputTests { @@ -48,4 +57,91 @@ 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 provideOsBuildNumbers() { + return Stream.of( + 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("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()); + } + }