fix(windows): enable VT processing so console output renders colours
`hermes setup` launched by install.ps1 on PowerShell 5.1/conhost printed `[35m`/`[0m` literally. hermes_cli.colors and the skins emit raw SGR codes whenever stdout is a TTY, but no Hermes entry point ever set ENABLE_VIRTUAL_TERMINAL_PROCESSING (origin/main didn't either; only pm/cli.py did, for its own progress line), and shells hand native children a console with VT off. hermes_bootstrap now opts the stdout/stderr console in at import, before anything prints and before the venv relaunch (the mode lives on the console buffer, so the child inherits it). Non-console handles are left alone; a console that refuses VT gets NO_COLOR so it shows plain text instead of escape garbage. ctypes only: colorama is merely transitive.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
"""Process bootstrap for Hermes entry points: Windows UTF-8 stdio, import-path
|
"""Process bootstrap for Hermes entry points: Windows UTF-8 stdio and ANSI console, import-path
|
||||||
hardening, durable lazy-install target, and dual-stack (Happy Eyeballs) connects.
|
hardening, durable lazy-install target, and dual-stack (Happy Eyeballs) connects.
|
||||||
|
|
||||||
Windows binds stdio to the console code page (cp1252), so ``print("café")`` raises
|
Windows binds stdio to the console code page (cp1252), so ``print("café")`` raises
|
||||||
@@ -253,6 +253,47 @@ def apply_windows_utf8_bootstrap() -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
_ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
||||||
|
|
||||||
|
|
||||||
|
def enable_windows_vt(streams=None) -> bool:
|
||||||
|
"""Opt the console behind stdout/stderr in to ANSI escape processing.
|
||||||
|
|
||||||
|
``hermes_cli.colors`` and the skins emit raw SGR codes whenever stdout is a TTY. A
|
||||||
|
conhost console (PowerShell 5.1, cmd.exe, the installer's ``hermes setup``) prints
|
||||||
|
them as ``←[35m`` until the output handle has ENABLE_VIRTUAL_TERMINAL_PROCESSING, and
|
||||||
|
shells hand native children a console with it off. The mode belongs to the console
|
||||||
|
buffer, so setting it here also covers the relaunched child. Handles that are not a
|
||||||
|
console (pipes, files, NUL, a windowless pythonw) fail GetConsoleMode and are left
|
||||||
|
alone. A console that refuses VT (pre-Windows 10) gets NO_COLOR instead, which
|
||||||
|
``should_use_color`` and rich honour, so it shows plain text rather than garbage.
|
||||||
|
Returns False only in that fallback case.
|
||||||
|
"""
|
||||||
|
if not _IS_WINDOWS:
|
||||||
|
return True
|
||||||
|
import ctypes
|
||||||
|
import msvcrt
|
||||||
|
from ctypes import wintypes
|
||||||
|
|
||||||
|
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||||
|
enabled = True
|
||||||
|
for stream in (sys.stdout, sys.stderr) if streams is None else streams:
|
||||||
|
try:
|
||||||
|
handle = msvcrt.get_osfhandle(stream.fileno())
|
||||||
|
except (AttributeError, OSError, ValueError):
|
||||||
|
continue # no fd (None under pythonw, StringIO in embedders)
|
||||||
|
mode = wintypes.DWORD()
|
||||||
|
if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
|
||||||
|
continue
|
||||||
|
if mode.value & _ENABLE_VIRTUAL_TERMINAL_PROCESSING:
|
||||||
|
continue
|
||||||
|
if not kernel32.SetConsoleMode(handle, mode.value | _ENABLE_VIRTUAL_TERMINAL_PROCESSING):
|
||||||
|
enabled = False
|
||||||
|
if not enabled:
|
||||||
|
os.environ.setdefault("NO_COLOR", "1")
|
||||||
|
return enabled
|
||||||
|
|
||||||
|
|
||||||
def suppress_platform_ver_console() -> None:
|
def suppress_platform_ver_console() -> None:
|
||||||
"""Stub ``platform._syscmd_ver`` on Windows — decode-crash + console-flash guard.
|
"""Stub ``platform._syscmd_ver`` on Windows — decode-crash + console-flash guard.
|
||||||
|
|
||||||
@@ -432,6 +473,7 @@ def export_scratch_tmp_env() -> None:
|
|||||||
# the very top of their module, before importing anything else. The
|
# the very top of their module, before importing anything else. The
|
||||||
# import side effect does the right thing.
|
# import side effect does the right thing.
|
||||||
apply_windows_utf8_bootstrap()
|
apply_windows_utf8_bootstrap()
|
||||||
|
enable_windows_vt()
|
||||||
suppress_platform_ver_console()
|
suppress_platform_ver_console()
|
||||||
install_never_free_environ()
|
install_never_free_environ()
|
||||||
|
|
||||||
|
|||||||
@@ -395,6 +395,49 @@ class TestHardenImportPath:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnableWindowsVt:
|
||||||
|
"""Hermes prints raw SGR codes; a conhost console renders them only with VT on."""
|
||||||
|
|
||||||
|
@pytest.mark.platforms("windows")
|
||||||
|
def test_turns_vt_on_for_a_console_that_has_it_off(self):
|
||||||
|
# A fresh console via CREATE_NEW_CONSOLE is the installer's situation:
|
||||||
|
# a real conhost whose output handle starts without VT processing.
|
||||||
|
script = textwrap.dedent("""
|
||||||
|
import ctypes, msvcrt, sys
|
||||||
|
from ctypes import wintypes
|
||||||
|
sys.path.insert(0, sys.argv[1])
|
||||||
|
import hermes_bootstrap
|
||||||
|
kernel32 = ctypes.WinDLL("kernel32")
|
||||||
|
handle = msvcrt.get_osfhandle(sys.stdout.fileno())
|
||||||
|
mode = wintypes.DWORD()
|
||||||
|
if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
|
||||||
|
sys.exit(4)
|
||||||
|
kernel32.SetConsoleMode(handle, mode.value & ~0x0004)
|
||||||
|
ok = hermes_bootstrap.enable_windows_vt()
|
||||||
|
kernel32.GetConsoleMode(handle, ctypes.byref(mode))
|
||||||
|
sys.exit(0 if ok and mode.value & 0x0004 else 3)
|
||||||
|
""").strip()
|
||||||
|
root = str(Path(__file__).resolve().parents[1])
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-c", script, root],
|
||||||
|
creationflags=subprocess.CREATE_NEW_CONSOLE,
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
if result.returncode == 4:
|
||||||
|
pytest.skip("this session cannot create a console")
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
@pytest.mark.platforms("windows")
|
||||||
|
def test_leaves_non_console_handles_and_colour_alone(self, tmp_path, monkeypatch):
|
||||||
|
# Redirected output must neither fail nor flip Hermes to NO_COLOR.
|
||||||
|
monkeypatch.delenv("NO_COLOR", raising=False)
|
||||||
|
import hermes_bootstrap
|
||||||
|
|
||||||
|
with open(tmp_path / "out.txt", "w", encoding="utf-8") as stream:
|
||||||
|
assert hermes_bootstrap.enable_windows_vt([stream]) is True
|
||||||
|
assert "NO_COLOR" not in os.environ
|
||||||
|
|
||||||
|
|
||||||
class TestSuppressPlatformVerConsole:
|
class TestSuppressPlatformVerConsole:
|
||||||
"""suppress_platform_ver_console: stub applied on Windows, no-op on POSIX."""
|
"""suppress_platform_ver_console: stub applied on Windows, no-op on POSIX."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user