fix(cli): keep info readable on a legacy Windows console
CI / python (push) Has been cancelled
CI / javascript (push) Has been cancelled
CI / dotnet (push) Has been cancelled

cmd.exe defaults to cp850/cp1252, which carry no check mark or arrow.
Printing one raised UnicodeEncodeError and aborted the report at the
Launch line, so everything after it was lost — including on the success
path, once the launch probe stopped failing.

Marks now degrade to plain text per glyph when the console cannot
encode them. UTF-8 consoles (Linux, macOS, Windows Terminal) keep the
original output byte for byte.

.NET substitutes rather than throwing here (verified on the reference
box), so only the Python wrapper needed the change.
This commit is contained in:
CloakHQ
2026-07-26 20:18:11 +02:00
parent 42924aaf99
commit 093a664601
3 changed files with 59 additions and 10 deletions
+1
View File
@@ -8,6 +8,7 @@ Changes are tagged: **[wrapper]** for Python/JS wrapper, **[binary]** for Chromi
## [Unreleased]
- **[wrapper]** **`cloakbrowser info` no longer aborts partway through on a Windows console.** `cmd.exe` defaults to a code page that cannot represent the report's check mark and arrow, so the command stopped with an encoding error at the first one and the remaining diagnostics were never printed. Those marks now degrade to plain text when the console cannot take them, per character. UTF-8 consoles, including Linux, macOS and Windows Terminal, are unchanged. Python.
- **[wrapper]** **`cloakbrowser info` no longer reports a false launch failure on Windows.** Chromium handles `--version` only on POSIX, so on Windows the switch was ignored and a browser started instead of printing — the probe then timed out and a healthy install was reported as broken, briefly putting a window on screen each run. The check now exits immediately on Windows, without a window, and still fails loudly on a genuinely broken binary; it reports no version there, since nothing is printed. Linux and macOS are unchanged. Python, JavaScript, and .NET.
## [0.5.2] — 2026-07-25
+34 -10
View File
@@ -18,8 +18,32 @@ import platform
import subprocess
import sys
UPGRADE_HINT = "For more than one concurrent session → https://cloakbrowser.dev"
FREE_LATEST_HINT = "Get the latest binary free → run 'cloakbrowser login' or https://cloakbrowser.dev/free"
def _console_glyph(glyph: str, fallback: str) -> str:
"""Return an ASCII stand-in when the console can't encode `glyph`.
Windows consoles default to cp850/cp1252, which carry none of the marks
below — printing one there raises UnicodeEncodeError and aborts the report
partway through. UTF-8 consoles keep the glyph.
"""
encoding = getattr(sys.stdout, "encoding", None) or "ascii"
try:
glyph.encode(encoding)
except (UnicodeEncodeError, LookupError):
return fallback
return glyph
MARK_OK = _console_glyph("", "OK")
MARK_FAIL = _console_glyph("", "x")
ARROW = _console_glyph("", "->")
DASH = _console_glyph("", "-")
UPGRADE_HINT = f"For more than one concurrent session {ARROW} https://cloakbrowser.dev"
FREE_LATEST_HINT = (
f"Get the latest binary free {ARROW} run 'cloakbrowser login' "
"or https://cloakbrowser.dev/free"
)
def _setup_logging() -> None:
@@ -326,7 +350,7 @@ def _print_diagnostics(diag: dict) -> None:
requested_channel = binary.get("requested_channel", "stable")
resolved_channel = binary.get("resolved_channel")
if binary.get("channel_fallback"):
print("Channel: Preview Stable fallback")
print(f"Channel: Preview {ARROW} Stable fallback")
elif resolved_channel:
print(f"Channel: {resolved_channel.capitalize()}")
else:
@@ -335,14 +359,14 @@ def _print_diagnostics(diag: dict) -> None:
if latest:
# Pro: show what launches now AND the server's latest, so the two
# can never silently diverge.
print(f"Version: {binary['version']} ({binary['tier']}) next launch")
print(f"Version: {binary['version']} ({binary['tier']}) {DASH} next launch")
if latest == binary["version"] and binary.get("installed"):
print(f"Latest: {latest} (up to date)")
elif latest == binary["version"]:
print(f"Latest: {latest} (downloads on next launch)")
elif binary.get("pinned"):
print(
f"Latest: {latest} (available pinned; unset "
f"Latest: {latest} (available {DASH} pinned; unset "
"CLOAKBROWSER_VERSION to upgrade)"
)
else:
@@ -354,7 +378,7 @@ def _print_diagnostics(diag: dict) -> None:
# Pro with no cached build and no server answer (e.g. offline).
print(
f"Version: not downloaded yet ({binary['tier']}) "
" next launch downloads the latest"
f"{DASH} next launch downloads the latest"
)
else:
print(f"Version: {binary['version']} ({binary['tier']})")
@@ -370,13 +394,13 @@ def _print_diagnostics(diag: dict) -> None:
print(f"Launch: {launch['reason']}")
elif launch["ok"]:
# Windows prints nothing, so say it ran rather than show an empty version.
print(f"Launch: {launch['version'] or 'runs (no version reported on Windows)'}")
print(f"Launch: {MARK_OK} {launch['version'] or 'runs (no version reported on Windows)'}")
else:
print(f"Launch: ✗ failed — {launch['error']}")
print(f"Launch: {MARK_FAIL} failed {DASH} {launch['error']}")
for lib in launch.get("missing_libs", []):
print(f" missing: {lib}")
if launch.get("missing_libs"):
print(" install the missing system libraries (e.g. apt-get install)")
print(f" {ARROW} install the missing system libraries (e.g. apt-get install)")
if "fonts" in diag:
win = diag["fonts"]["windows"]
@@ -387,7 +411,7 @@ def _print_diagnostics(diag: dict) -> None:
verdict = "ok" if n == total else "missing" if n == 0 else "partial"
print(f"Win fonts: {verdict} ({n}/{total})")
if n < total:
print(" incomplete Windows font set; copy real Windows fonts (Segoe UI, Calibri, Consolas)")
print(f" {ARROW} incomplete Windows font set; copy real Windows fonts (Segoe UI, Calibri, Consolas)")
office = diag["fonts"].get("office")
if office is not None:
n, total = office
+24
View File
@@ -220,6 +220,30 @@ def test_binary_version_runs_stub(tmp_path):
assert err == ""
def test_console_glyph_falls_back_on_legacy_windows_encoding(monkeypatch):
"""cmd.exe defaults to cp850/cp1252, which carry no check mark — printing one
there aborted the whole report with UnicodeEncodeError."""
import io
monkeypatch.setattr(
cloakbrowser.__main__.sys, "stdout", io.TextIOWrapper(io.BytesIO(), encoding="cp1252")
)
assert cloakbrowser.__main__._console_glyph("", "OK") == "OK"
assert cloakbrowser.__main__._console_glyph("", "->") == "->"
# cp1252 does carry the em dash, so it is kept — the check is per glyph.
assert cloakbrowser.__main__._console_glyph("", "-") == ""
def test_console_glyph_kept_on_utf8(monkeypatch):
import io
monkeypatch.setattr(
cloakbrowser.__main__.sys, "stdout", io.TextIOWrapper(io.BytesIO(), encoding="utf-8")
)
assert cloakbrowser.__main__._console_glyph("", "OK") == ""
assert cloakbrowser.__main__._console_glyph("", "x") == ""
@pytestmark_posix
def test_binary_version_windows_probe_does_not_hang(tmp_path, monkeypatch):
"""Real Windows Chrome ignores --version and starts a browser instead of