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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
_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:
|
||||
"""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
|
||||
# import side effect does the right thing.
|
||||
apply_windows_utf8_bootstrap()
|
||||
enable_windows_vt()
|
||||
suppress_platform_ver_console()
|
||||
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:
|
||||
"""suppress_platform_ver_console: stub applied on Windows, no-op on POSIX."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user