Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -31,6 +35,7 @@
*
* @author Phillip Webb
* @author Yong-Hyun Kim
* @author Philemon Hilscher
* @since 1.0.0
*/
public abstract class AnsiOutput {
Expand All @@ -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";
Expand Down Expand Up @@ -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<String> 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<Integer> parseWindowsBuildNumber(Supplier<String> 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -48,4 +57,91 @@ void encoding() {
assertThat(encoded).isEqualTo("ABDEF");
}

private static Stream<Arguments> 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<Arguments> 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<Arguments> 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<Integer> parsed = AnsiOutput.parseWindowsBuildNumber(() -> registryResult);
assertTrue(parsed.isPresent());
assertEquals(expected, parsed.get());
}

}