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:
1 parent
4feac70c91
commit
cd4a2351ae
8 files changed
+536
-21
No files matched your search
+168
-1
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user