Auto headless + Xvfb for headless servers

Enable Chromium on machines without DISPLAY: Python auto headless
with Xvfb hints, and start.sh installs/starts Xvfb (xvfb-run or
persistent :99) so registration can run headed under a virtual display.
This commit is contained in:
chaos committed 2026-07-12 00:34:15 +08:00
1 parent 4feac70c91
commit cd4a2351ae
8 files changed
+536 -21

No files matched your search

+168 -1
View File
@@ -57,6 +57,8 @@ DEFAULT_CONFIG = {
"cloudmail_admin_email": "",
"cloudmail_password": "",
"cpa_gui_close_mint_browser": True,
"browser_headless": "auto",
"browser_path": "",
"hotmail_accounts_file": "mail_credentials.txt",
"hotmail_alias_mode": "random",
"hotmail_alias_random_length": 8,
@@ -622,6 +624,154 @@ CHROMIUM_SLIM_FLAGS = [
"--disable-blink-features=AutomationControlled",
]
# Headless / display helpers (Linux servers without desktop)
_HEADLESS_HINT_SHOWN = False
_HEADLESS_HINT_LOCK = threading.Lock()
def has_display() -> bool:
"""Return True if a GUI display is likely available for Chromium.
- Windows: always True (Win32 GUI subsystem).
- Linux/macOS: need DISPLAY or WAYLAND_DISPLAY (Xvfb counts as DISPLAY).
"""
if sys.platform == "win32":
return True
display = (os.environ.get("DISPLAY") or "").strip()
wayland = (os.environ.get("WAYLAND_DISPLAY") or "").strip()
return bool(display or wayland)
def _truthy_headless(val) -> bool | None:
"""Parse headless preference. True/False explicit; None means auto."""
if val is None:
return None
if isinstance(val, bool):
return val
s = str(val).strip().lower()
if s in ("", "auto", "default", "detect"):
return None
if s in ("1", "true", "yes", "on", "headless"):
return True
if s in ("0", "false", "no", "off", "headed", "gui"):
return False
return None
def resolve_headless_preference(preference=None) -> bool | None:
"""Resolve headless preference from arg → env → config → auto.
Returns:
True = force headless
False = force headed
None = auto (headless when no DISPLAY)
"""
if preference is not None:
parsed = _truthy_headless(preference)
if parsed is not None or str(preference).strip().lower() in ("", "auto", "default", "detect"):
return parsed
env_raw = (os.environ.get("BROWSER_HEADLESS") or os.environ.get("GROK_HEADLESS") or "").strip()
if env_raw:
return _truthy_headless(env_raw)
try:
cfg_val = config.get("browser_headless", "auto") if isinstance(config, dict) else "auto"
except Exception:
cfg_val = "auto"
return _truthy_headless(cfg_val)
def should_use_headless(preference=None, *, log_hint: bool = True) -> bool:
"""Decide whether Chromium should start headless.
Auto (default): headless when no DISPLAY/WAYLAND_DISPLAY.
Force headed without display is impossible — still headless + warn.
"""
pref = resolve_headless_preference(preference)
display_ok = has_display()
if pref is True:
if log_hint:
log_headless_mode(forced=True, auto=False, display_ok=display_ok)
return True
if pref is False:
if display_ok:
return False
# Cannot start headed without display — force headless
if log_hint:
log_headless_mode(forced=True, auto=True, display_ok=False, wanted_headed=True)
return True
# auto
use = not display_ok
if use and log_hint:
log_headless_mode(forced=False, auto=True, display_ok=False)
return use
def log_headless_mode(
*,
forced: bool = False,
auto: bool = False,
display_ok: bool = False,
wanted_headed: bool = False,
) -> None:
"""Print one-shot guidance for headless / Xvfb on servers."""
global _HEADLESS_HINT_SHOWN
with _HEADLESS_HINT_LOCK:
if _HEADLESS_HINT_SHOWN:
return
_HEADLESS_HINT_SHOWN = True
disp = (os.environ.get("DISPLAY") or "").strip() or "(unset)"
wayland = (os.environ.get("WAYLAND_DISPLAY") or "").strip() or "(unset)"
if wanted_headed and not display_ok:
print(
f"[!] browser_headless=false/--headed 但无图形界面 "
f"(DISPLAY={disp}, WAYLAND_DISPLAY={wayland}),仍强制 headless,否则无法启动。",
flush=True,
)
elif auto and not display_ok:
print(
f"[*] 无图形界面 (DISPLAY={disp}),已自动启用 --headless=new。",
flush=True,
)
elif forced:
print("[*] 已强制启用 headless 浏览器。", flush=True)
else:
return
print(
" 提示: headless 更容易被 Cloudflare/Turnstile 拦截;无头服务器推荐虚拟显示:\n"
" sudo apt install -y xvfb\n"
" xvfb-run -a uv run python -u register_cli.py --extra 1 --threads 1\n"
" 或常驻: Xvfb :99 -screen 0 1920x1080x24 & export DISPLAY=:99\n"
" 强制无头: config browser_headless=true / env BROWSER_HEADLESS=1 / CLI --headless\n"
" 强制有头: config browser_headless=false / CLI --headed(需 DISPLAY 或 xvfb-run)",
flush=True,
)
def apply_headless_to_options(options, headless: bool = True) -> None:
"""Apply headless flags to ChromiumOptions (DrissionPage)."""
if not headless:
try:
options.headless(False)
except Exception:
pass
return
try:
options.headless(True)
except Exception:
try:
options.set_argument("--headless=new")
except Exception:
try:
options.set_argument("--headless")
except Exception:
pass
# Extra flags commonly needed on Linux servers
for flag in ("--headless=new", "--disable-gpu", "--no-sandbox", "--disable-dev-shm-usage"):
try:
options.set_argument(flag)
except Exception:
pass
# Prefer Google Chrome for registration reliability (email delivery path).
# Chromium is fallback; turnstilePatch extension only loads on Chromium.
# On Chrome we inject the same patch via CDP add_init_js (see apply_stealth_patches).
@@ -723,7 +873,14 @@ def apply_stealth_patches(page=None, log_callback=None) -> bool:
return ok
def create_browser_options():
def create_browser_options(headless=None):
"""Build ChromiumOptions for register / TabPool.
headless:
None → resolve from config/env/auto (no DISPLAY → headless)
True → force headless
False → prefer headed (still headless if no DISPLAY)
"""
options = ChromiumOptions()
options.auto_port()
options.set_timeouts(base=1)
@@ -766,6 +923,16 @@ def create_browser_options():
options.set_argument(f"--proxy-server={scheme}://{host}:{port}")
except Exception as e:
print(f" [proxy] set browser proxy failed: {e}")
use_hl = should_use_headless(headless, log_hint=True)
apply_headless_to_options(options, use_hl)
if use_hl:
print(" [browser] headless=True", flush=True)
else:
print(
f" [browser] headed DISPLAY={os.environ.get('DISPLAY', '')!r}",
flush=True,
)
return options