feat: info/doctor reports Windows + Office font completeness

Windows font check now covers the full 8-font set (adds the two monospace
fonts) and reports a strict N/8 count; the launch-time warning fires on any
incomplete set, not just when none are present. Adds a separate Office-font
group (10 fonts) reported as an informational N/10 with no install nudge.

Ported across all three wrappers (Python, JS, .NET) to keep them in sync.
This commit is contained in:
CloakHQ committed 2026-07-07 21:58:01 +02:00
1 parent 5a1e2978e4
commit ae7fd43be7
10 files changed
+254 -71

No files matched your search

+27 -7
View File
@@ -205,11 +205,19 @@ def _collect_diagnostics(quick: bool) -> dict:
# Windows-font probe — only meaningful on a Linux host spoofing Windows.
# Omitted entirely off Linux, where it carries no signal.
if platform.system() == "Linux":
from .browser import _windows_fonts_present
from .browser import (
_OFFICE_FONT_TELLS,
_WINDOWS_FONT_TELLS,
_count_fonts_present,
)
present = _windows_fonts_present()
# Strict count, not "any one present" — real font installs are atomic
# (you have the whole pack or none), so report how complete the set is.
win_n = _count_fonts_present(_WINDOWS_FONT_TELLS)
office_n = _count_fonts_present(_OFFICE_FONT_TELLS)
diag["fonts"] = {
"windows_fonts": {True: "ok", False: "missing", None: "unknown"}[present]
"windows": None if win_n is None else [win_n, len(_WINDOWS_FONT_TELLS)],
"office": None if office_n is None else [office_n, len(_OFFICE_FONT_TELLS)],
}
diag["license"] = license_info
@@ -270,10 +278,22 @@ def _print_diagnostics(diag: dict) -> None:
print(" → install the missing system libraries (e.g. apt-get install)")
if "fonts" in diag:
fonts = diag["fonts"]["windows_fonts"]
print(f"Win fonts: {fonts}")
if fonts == "missing":
print(" → spoofing Windows on Linux without Windows fonts; install msttcorefonts")
win = diag["fonts"]["windows"]
if win is None:
print("Win fonts: unknown (fc-list unavailable)")
else:
n, total = win
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)")
office = diag["fonts"].get("office")
if office is not None:
n, total = office
# Office is informational only — no Office pack is a normal Windows
# persona (~53% of real machines have none), so no install nudge.
verdict = "ok" if n == total else "absent" if n == 0 else "partial"
print(f"Office fonts: {verdict} ({n}/{total})")
lic = diag["license"]
tier = lic["tier"]
+44 -11
View File
@@ -1171,6 +1171,10 @@ def build_args(
# Microsoft-proprietary fonts that signal a real Windows install (absent from
# ttf-mscorefonts-installer). Keep in sync with issue #395 and
# docs/chrome40-fpjs-font-minimum-set-investigation.md.
# Windows OS fonts — ship with Windows itself, so their absence on a
# Windows-spoofing Linux host degrades results. The two monospace fonts
# (Consolas + Courier New) are part of the recommended set so the generic
# `monospace` family resolves to a Windows font. See issue #395.
_WINDOWS_FONT_TELLS = (
"Segoe UI",
"Segoe UI Light",
@@ -1178,17 +1182,36 @@ _WINDOWS_FONT_TELLS = (
"Marlett",
"MS UI Gothic",
"Franklin Gothic",
"Consolas",
"Courier New",
)
# MS Office supplemental fonts, installed as one atomic block by every Office
# install. Roughly half of real Windows machines have this pack and half do
# not, so its absence is a perfectly normal Windows setup, NOT a problem —
# reported as an informational signal only, never a warning.
_OFFICE_FONT_TELLS = (
"MT Extra",
"Century",
"Century Gothic",
"MS Reference Specialty",
"Wingdings 2",
"Wingdings 3",
"Book Antiqua",
"Bookshelf Symbol 7",
"Monotype Corsiva",
"Bookman Old Style",
)
_font_warning_checked = False
def _windows_fonts_present() -> bool | None:
"""Probe for Windows fonts via fc-list.
def _count_fonts_present(tells: tuple[str, ...]) -> int | None:
"""Count how many tell-tale fonts are installed, via fc-list.
Tri-state: True if any tell-tale font is installed, False if none found,
None if it can't be determined (fc-list missing or errored). Callers must
NOT warn on None — only an explicit False means "no Windows fonts".
Returns the number present (0..len(tells)), or None if it can't be
determined (fc-list missing or errored). Callers must NOT treat None as
zero — None means "unknown", 0 means "genuinely none installed".
"""
import subprocess
try:
@@ -1200,11 +1223,21 @@ def _windows_fonts_present() -> bool | None:
if result.returncode != 0:
return None
listing = result.stdout.lower()
return any(font.lower() in listing for font in _WINDOWS_FONT_TELLS)
return sum(1 for font in tells if font.lower() in listing)
def _windows_fonts_present() -> bool | None:
"""True if ALL Windows OS fonts are installed, False if any are missing,
None if unknown. Strict: a partial set is treated as incomplete, since the
font install is atomic and a missing font degrades the Windows persona.
"""
n = _count_fonts_present(_WINDOWS_FONT_TELLS)
return None if n is None else n == len(_WINDOWS_FONT_TELLS)
def _maybe_warn_windows_fonts(chrome_args: list[str]) -> None:
"""Warn once when spoofing Windows on a Linux host with no Windows fonts.
"""Warn once when spoofing Windows on a Linux host without the full Windows
font set.
Best-effort and silent on error — never raises. Gated by an in-process flag
plus a cache-dir marker so it fires at most once per environment. Suppress
@@ -1234,13 +1267,13 @@ def _maybe_warn_windows_fonts(chrome_args: list[str]) -> None:
return
present = _windows_fonts_present()
if present is None or present is True:
return # fonts present, or can't determine — don't warn
return # full set present, or can't determine — don't warn
# Write straight to stderr (like the welcome banner and the JS/.NET
# wrappers) so an app's logging config can't silence it.
sys.stderr.write(
"[cloakbrowser] No Windows fonts found — installing them is strongly "
"advised for best results when spoofing Windows on Linux. "
"https://github.com/CloakHQ/cloakbrowser#font-setup-on-linux "
"[cloakbrowser] Incomplete Windows font set — installing the full "
"set is strongly advised for best results when spoofing Windows on "
"Linux. https://github.com/CloakHQ/cloakbrowser#font-setup-on-linux "
"(silence: CLOAKBROWSER_SUPPRESS_FONT_WARNING=1)\n"
)
try:
+20 -4
View File
@@ -132,10 +132,26 @@ static void PrintDiagnostics(Dictionary<string, object?> diag)
if (diag.TryGetValue("fonts", out var fontsObj) && fontsObj is Dictionary<string, object?> fonts)
{
string winFonts = (string)fonts["windows_fonts"]!;
Console.WriteLine($"Win fonts: {winFonts}");
if (winFonts == "missing")
Console.WriteLine(" → spoofing Windows on Linux without Windows fonts; install msttcorefonts");
if (fonts["windows"] is int[] win)
{
int n = win[0], total = win[1];
string verdict = n == total ? "ok" : n == 0 ? "missing" : "partial";
Console.WriteLine($"Win fonts: {verdict} ({n}/{total})");
if (n < total)
Console.WriteLine(" → incomplete Windows font set; copy real Windows fonts (Segoe UI, Calibri, Consolas)");
}
else
{
Console.WriteLine("Win fonts: unknown (fc-list unavailable)");
}
// Office is informational only — no Office pack is a normal Windows
// persona (~53% of real machines have none), so no install nudge.
if (fonts.TryGetValue("office", out var officeObj) && officeObj is int[] office)
{
int n = office[0], total = office[1];
string verdict = n == total ? "ok" : n == 0 ? "absent" : "partial";
Console.WriteLine($"Office fonts: {verdict} ({n}/{total})");
}
}
var lic = (Dictionary<string, object?>)diag["license"]!;
+43 -19
View File
@@ -358,23 +358,36 @@ public static class CloakLauncher
// environment. See docs/chrome40-fpjs-font-minimum-set-investigation.md.
// -----------------------------------------------------------------------
// Microsoft-proprietary fonts that signal a real Windows install (absent from
// ttf-mscorefonts-installer). Keep in sync with issue #395 and
// docs/chrome40-fpjs-font-minimum-set-investigation.md.
private static readonly string[] WindowsFontTells =
// Windows OS fonts — ship with Windows itself, so their absence on a
// Windows-spoofing Linux host degrades results. The two monospace fonts
// (Consolas + Courier New) are part of the recommended set so the generic
// `monospace` family resolves to a Windows font. See issue #395.
internal static readonly string[] WindowsFontTells =
{
"Segoe UI", "Segoe UI Light", "Calibri", "Marlett", "MS UI Gothic", "Franklin Gothic",
"Segoe UI", "Segoe UI Light", "Calibri", "Marlett", "MS UI Gothic",
"Franklin Gothic", "Consolas", "Courier New",
};
// MS Office supplemental fonts, installed as one atomic block by every Office
// install. Roughly half of real Windows machines have this pack and half do
// not, so its absence is a perfectly normal Windows setup, NOT a problem —
// reported as an informational signal only, never a warning.
internal static readonly string[] OfficeFontTells =
{
"MT Extra", "Century", "Century Gothic", "MS Reference Specialty",
"Wingdings 2", "Wingdings 3", "Book Antiqua", "Bookshelf Symbol 7",
"Monotype Corsiva", "Bookman Old Style",
};
internal static bool _fontWarningChecked;
/// <summary>
/// Probe for Windows fonts via fc-list. Tri-state: true if any tell-tale font
/// is installed, false if none found, null if it can't be determined (fc-list
/// missing or errored). Callers must NOT warn on null — only an explicit false
/// means "no Windows fonts".
/// Count how many tell-tale fonts are installed, via fc-list. Returns the
/// number present (0..tells.Length), or null if it can't be determined
/// (fc-list missing or errored). Callers must NOT treat null as zero — null
/// means "unknown", 0 means "genuinely none installed".
/// </summary>
internal static bool? WindowsFontsPresent()
internal static int? CountFontsPresent(string[] tells)
{
string output;
try
@@ -411,14 +424,25 @@ public static class CloakLauncher
return null;
}
var listing = output.ToLowerInvariant();
return WindowsFontTells.Any(f => listing.Contains(f.ToLowerInvariant()));
return tells.Count(f => listing.Contains(f.ToLowerInvariant()));
}
/// <summary>
/// Warn once when spoofing Windows on a Linux host with no Windows fonts.
/// Best-effort and silent on error — never throws. Gated by an in-process flag
/// plus a cache-dir marker so it fires at most once per environment. Suppress
/// entirely with CLOAKBROWSER_SUPPRESS_FONT_WARNING.
/// True if ALL Windows OS fonts are installed, false if any are missing, null
/// if unknown. Strict: a partial set is treated as incomplete, since the font
/// install is atomic and a missing font degrades the Windows persona.
/// </summary>
internal static bool? WindowsFontsPresent()
{
var n = CountFontsPresent(WindowsFontTells);
return n is null ? null : n == WindowsFontTells.Length;
}
/// <summary>
/// Warn once when spoofing Windows on a Linux host without the full Windows
/// font set. Best-effort and silent on error — never throws. Gated by an
/// in-process flag plus a cache-dir marker so it fires at most once per
/// environment. Suppress entirely with CLOAKBROWSER_SUPPRESS_FONT_WARNING.
/// </summary>
internal static void MaybeWarnWindowsFonts(IReadOnlyList<string> chromeArgs)
{
@@ -441,11 +465,11 @@ public static class CloakLauncher
var marker = Path.Combine(Config.GetCacheDir(), ".font_warning_shown");
if (File.Exists(marker)) return;
var present = WindowsFontsPresent();
if (present != false) return; // true (present) or null (undeterminable)
if (present != false) return; // true (full set) or null (undeterminable)
CloakLog.Warning(
"[cloakbrowser] No Windows fonts found — installing them is strongly " +
"advised for best results when spoofing Windows on Linux. " +
"https://github.com/CloakHQ/cloakbrowser#font-setup-on-linux " +
"[cloakbrowser] Incomplete Windows font set — installing the full " +
"set is strongly advised for best results when spoofing Windows on " +
"Linux. https://github.com/CloakHQ/cloakbrowser#font-setup-on-linux " +
"(silence: CLOAKBROWSER_SUPPRESS_FONT_WARNING=1)");
try
{
+6 -2
View File
@@ -63,10 +63,14 @@ internal static class Diagnostics
// Omitted entirely off Linux, where it carries no signal.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
bool? present = CloakLauncher.WindowsFontsPresent();
// Strict count, not "any one present" — real font installs are atomic
// (you have the whole pack or none), so report how complete the set is.
int? winN = CloakLauncher.CountFontsPresent(CloakLauncher.WindowsFontTells);
int? officeN = CloakLauncher.CountFontsPresent(CloakLauncher.OfficeFontTells);
diag["fonts"] = new Dictionary<string, object?>
{
["windows_fonts"] = present == true ? "ok" : present == false ? "missing" : "unknown",
["windows"] = winN is null ? null : new[] { winN.Value, CloakLauncher.WindowsFontTells.Length },
["office"] = officeN is null ? null : new[] { officeN.Value, CloakLauncher.OfficeFontTells.Length },
};
}
+25 -7
View File
@@ -21,7 +21,7 @@ import {
normalizeRequestedVersion,
CHROMIUM_VERSION,
} from "./config.js";
import { windowsFontsPresent } from "./fonts.js";
import { countFontsPresent, WINDOWS_FONT_TELLS, OFFICE_FONT_TELLS } from "./fonts.js";
import { resolveLicenseKey, validateLicense, getProLatestVersion, type LicenseInfo } from "./license.js";
import { execFileSync } from "node:child_process";
import { createRequire } from "node:module";
@@ -192,9 +192,13 @@ export async function collectDiagnostics(quick: boolean): Promise<Record<string,
// Windows-font probe — only meaningful on a Linux host spoofing Windows.
// Omitted entirely off Linux, where it carries no signal.
if (os.platform() === "linux") {
const present = windowsFontsPresent();
// Strict count, not "any one present" — real font installs are atomic
// (you have the whole pack or none), so report how complete the set is.
const winN = countFontsPresent(WINDOWS_FONT_TELLS);
const officeN = countFontsPresent(OFFICE_FONT_TELLS);
diag.fonts = {
windows_fonts: present === true ? "ok" : present === false ? "missing" : "unknown",
windows: winN === null ? null : [winN, WINDOWS_FONT_TELLS.length],
office: officeN === null ? null : [officeN, OFFICE_FONT_TELLS.length],
};
}
@@ -254,10 +258,24 @@ function printDiagnostics(diag: Record<string, any>): void {
}
if (diag.fonts) {
const fonts = diag.fonts.windows_fonts;
console.log(`Win fonts: ${fonts}`);
if (fonts === "missing") {
console.log(" → spoofing Windows on Linux without Windows fonts; install msttcorefonts");
const win = diag.fonts.windows;
if (win === null) {
console.log("Win fonts: unknown (fc-list unavailable)");
} else {
const [n, total] = win;
const verdict = n === total ? "ok" : n === 0 ? "missing" : "partial";
console.log(`Win fonts: ${verdict} (${n}/${total})`);
if (n < total) {
console.log(" → incomplete Windows font set; copy real Windows fonts (Segoe UI, Calibri, Consolas)");
}
}
const office = diag.fonts.office;
if (office != null) {
const [n, total] = office;
// Office is informational only — no Office pack is a normal Windows
// persona (~53% of real machines have none), so no install nudge.
const verdict = n === total ? "ok" : n === 0 ? "absent" : "partial";
console.log(`Office fonts: ${verdict} (${n}/${total})`);
}
}
+45 -14
View File
@@ -13,28 +13,48 @@ import os from "node:os";
import path from "node:path";
import { getCacheDir } from "./config.js";
// Microsoft-proprietary fonts that signal a real Windows install (absent from
// ttf-mscorefonts-installer). Keep in sync with issue #395 and
// docs/chrome40-fpjs-font-minimum-set-investigation.md.
const WINDOWS_FONT_TELLS = [
// Windows OS fonts — ship with Windows itself, so their absence on a
// Windows-spoofing Linux host degrades results. The two monospace fonts
// (Consolas + Courier New) are part of the recommended set so the generic
// `monospace` family resolves to a Windows font. See issue #395.
export const WINDOWS_FONT_TELLS = [
"Segoe UI",
"Segoe UI Light",
"Calibri",
"Marlett",
"MS UI Gothic",
"Franklin Gothic",
"Consolas",
"Courier New",
];
// MS Office supplemental fonts, installed as one atomic block by every Office
// install. Roughly half of real Windows machines have this pack and half do
// not, so its absence is a perfectly normal Windows setup, NOT a problem —
// reported as an informational signal only, never a warning.
export const OFFICE_FONT_TELLS = [
"MT Extra",
"Century",
"Century Gothic",
"MS Reference Specialty",
"Wingdings 2",
"Wingdings 3",
"Book Antiqua",
"Bookshelf Symbol 7",
"Monotype Corsiva",
"Bookman Old Style",
];
let fontWarningChecked = false;
/**
* Probe for Windows fonts via fc-list.
* Count how many tell-tale fonts are installed, via fc-list.
*
* Tri-state: true if any tell-tale font is installed, false if none found,
* null if it can't be determined (fc-list missing or errored). Callers must
* NOT warn on null — only an explicit false means "no Windows fonts".
* Returns the number present (0..tells.length), or null if it can't be
* determined (fc-list missing or errored). Callers must NOT treat null as
* zero — null means "unknown", 0 means "genuinely none installed".
*/
export function windowsFontsPresent(): boolean | null {
export function countFontsPresent(tells: string[]): number | null {
let listing: string;
try {
// maxBuffer 16 MB: a host with a large font set can produce an fc-list
@@ -44,11 +64,22 @@ export function windowsFontsPresent(): boolean | null {
} catch {
return null;
}
return WINDOWS_FONT_TELLS.some((f) => listing.includes(f.toLowerCase()));
return tells.filter((f) => listing.includes(f.toLowerCase())).length;
}
/**
* Warn once when spoofing Windows on a Linux host with no Windows fonts.
* True if ALL Windows OS fonts are installed, false if any are missing, null
* if unknown. Strict: a partial set is treated as incomplete, since the font
* install is atomic and a missing font degrades the Windows persona.
*/
export function windowsFontsPresent(): boolean | null {
const n = countFontsPresent(WINDOWS_FONT_TELLS);
return n === null ? null : n === WINDOWS_FONT_TELLS.length;
}
/**
* Warn once when spoofing Windows on a Linux host without the full Windows
* font set.
*
* Best-effort and silent on error — never throws. Gated by an in-process flag
* plus a cache-dir marker so it fires at most once per environment. Suppress
@@ -73,10 +104,10 @@ export function maybeWarnWindowsFonts(chromeArgs: string[]): void {
const marker = path.join(getCacheDir(), ".font_warning_shown");
if (fs.existsSync(marker)) return;
const present = windowsFontsPresent();
if (present === null || present === true) return; // present or undeterminable
if (present === null || present === true) return; // full set present or undeterminable
console.warn(
"[cloakbrowser] No Windows fonts found — installing them is strongly " +
"advised for best results when spoofing Windows on Linux. " +
"[cloakbrowser] Incomplete Windows font set — installing the full set " +
"is strongly advised for best results when spoofing Windows on Linux. " +
"https://github.com/CloakHQ/cloakbrowser#font-setup-on-linux " +
"(silence: CLOAKBROWSER_SUPPRESS_FONT_WARNING=1)",
);
+1 -1
View File
@@ -41,7 +41,7 @@ describe("collectDiagnostics", () => {
expect(diag.binary).toBeDefined();
// fonts section only present on Linux
if (os.platform() === "linux") expect(diag.fonts.windows_fonts).toBeTruthy();
if (os.platform() === "linux") expect(diag.fonts.windows).toBeDefined();
else expect(diag.fonts).toBeUndefined();
expect(typeof diag.geoip.db_present).toBe("boolean");
expect(Object.keys(diag.modules).length).toBeGreaterThan(0);
+21 -3
View File
@@ -12,7 +12,14 @@ import path from "node:path";
vi.mock("node:child_process", () => ({ execFileSync: vi.fn() }));
const WIN_ARGS = ["--fingerprint-platform=windows", "--no-sandbox"];
const MSG = "No Windows fonts found";
const MSG = "Incomplete Windows font set";
// fc-list output containing all 8 Windows tell fonts (the complete set).
const ALL_WIN_FONTS =
"Segoe UI:style=Regular\nSegoe UI Light:style=Light\nCalibri:style=Regular\n" +
"Marlett:style=Regular\nMS UI Gothic:style=Regular\n" +
"Franklin Gothic Medium:style=Regular\nConsolas:style=Regular\n" +
"Courier New:style=Regular";
let cacheDir: string;
@@ -111,12 +118,23 @@ describe("maybeWarnWindowsFonts", () => {
expect(fs.existsSync(marker())).toBe(false);
});
it("does not warn when Windows fonts are present", async () => {
it("does not warn when the full Windows set is present", async () => {
vi.spyOn(os, "platform").mockReturnValue("linux");
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const { maybeWarn, execFileSync } = await load();
execFileSync.mockReturnValue("/x/segoeui.ttf: Segoe UI:style=Regular" as any);
execFileSync.mockReturnValue(ALL_WIN_FONTS as any);
maybeWarn(WIN_ARGS);
expect(warn).not.toHaveBeenCalled();
});
it("warns on a partial Windows set (strict — all 8 required)", async () => {
vi.spyOn(os, "platform").mockReturnValue("linux");
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const { maybeWarn, execFileSync } = await load();
// Only 1 of the 8 tells present.
execFileSync.mockReturnValue("/x/segoeui.ttf: Segoe UI:style=Regular" as any);
maybeWarn(WIN_ARGS);
expect(warn).toHaveBeenCalledTimes(1);
expect(String(warn.mock.calls[0][0])).toContain(MSG);
});
});
+22 -3
View File
@@ -14,7 +14,15 @@ import pytest
import cloakbrowser.browser as browser
WIN_ARGS = ["--fingerprint-platform=windows", "--no-sandbox"]
MSG = "No Windows fonts found"
MSG = "Incomplete Windows font set"
# fc-list output containing all 8 Windows tell fonts (the complete set).
ALL_WIN_FONTS = (
"Segoe UI:style=Regular\nSegoe UI Light:style=Light\nCalibri:style=Regular\n"
"Marlett:style=Regular\nMS UI Gothic:style=Regular\n"
"Franklin Gothic Medium:style=Regular\nConsolas:style=Regular\n"
"Courier New:style=Regular"
)
@pytest.fixture(autouse=True)
@@ -110,7 +118,18 @@ def test_no_warn_no_crash_when_fc_list_absent(tmp_path, capsys):
assert not (tmp_path / ".font_warning_shown").exists()
def test_no_warn_when_fonts_present(tmp_path, capsys):
def test_no_warn_when_full_set_present(tmp_path, capsys):
with (
patch("platform.system", return_value="Linux"),
patch("subprocess.run", side_effect=_fc_list(0, ALL_WIN_FONTS)),
patch("cloakbrowser.config.get_cache_dir", return_value=tmp_path),
):
browser._maybe_warn_windows_fonts(WIN_ARGS)
assert MSG not in capsys.readouterr().err
def test_warns_on_partial_set(tmp_path, capsys):
# Only 1 of the 8 tells present — strict check treats this as incomplete.
listing = "/usr/share/fonts/segoeui.ttf: Segoe UI:style=Regular"
with (
patch("platform.system", return_value="Linux"),
@@ -118,4 +137,4 @@ def test_no_warn_when_fonts_present(tmp_path, capsys):
patch("cloakbrowser.config.get_cache_dir", return_value=tmp_path),
):
browser._maybe_warn_windows_fonts(WIN_ARGS)
assert MSG not in capsys.readouterr().err
assert MSG in capsys.readouterr().err