mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 18:07:24 +01:00
Drives a Flutter web build in a persistent headless chromium (systemd-run unit, own profile, CDP port) through the semantics tree: tree, tap by label, text, key, scroll, wait, shot, console. Never touches the live display; attaches only to HeadlessChrome. The MCP server wraps the same Driver. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZ4sULGfyav7EEwQ6vhbfg
82 lines
2.4 KiB
Python
82 lines
2.4 KiB
Python
"""The headless chromium unit: argv, systemd-run wrapper, size parsing."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import subprocess
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
|
|
from flutter_web_drive.errors import EXIT_USAGE, DriveError
|
|
|
|
# The real binary: /usr/local/bin/chromium is a hosts-installer wrapper.
|
|
CHROMIUM = "/usr/bin/chromium"
|
|
|
|
Runner = Callable[[list[str]], int]
|
|
|
|
|
|
def run(cmd: list[str]) -> int:
|
|
"""Run a short command quietly and return its exit code."""
|
|
return subprocess.run(cmd, check=False, capture_output=True).returncode
|
|
|
|
|
|
def parse_size(size: str) -> tuple[int, int]:
|
|
"""``1400x1000`` -> (1400, 1000)."""
|
|
m = re.fullmatch(r"(\d+)x(\d+)", size)
|
|
if not m:
|
|
raise DriveError(
|
|
"start", f"--size must look like 1400x1000, got {size!r}", EXIT_USAGE
|
|
)
|
|
return int(m[1]), int(m[2])
|
|
|
|
|
|
def chromium_argv(
|
|
url: str, profile: Path, cdp_port: int, size: tuple[int, int]
|
|
) -> list[str]:
|
|
"""Headless chromium with CDP on loopback and a persistent profile.
|
|
|
|
``--disable-extensions`` keeps the Claude extension (loaded via
|
|
chromium-flags.conf) out of this CDP session; ``--password-store=basic``
|
|
stops chromium asking the desktop keyring, which could prompt on :0.
|
|
"""
|
|
width, height = size
|
|
return [
|
|
CHROMIUM,
|
|
"--headless=new",
|
|
f"--remote-debugging-port={cdp_port}",
|
|
"--remote-debugging-address=127.0.0.1",
|
|
f"--user-data-dir={profile}",
|
|
"--disable-extensions",
|
|
"--no-first-run",
|
|
"--no-default-browser-check",
|
|
"--password-store=basic",
|
|
"--mute-audio",
|
|
"--hide-scrollbars",
|
|
"--enable-unsafe-swiftshader",
|
|
f"--window-size={width},{height}",
|
|
url,
|
|
]
|
|
|
|
|
|
def unit_argv(unit: str, argv: list[str]) -> list[str]:
|
|
"""``systemd-run --user`` so the browser outlives the calling shell.
|
|
|
|
DISPLAY/WAYLAND_DISPLAY are unset in the unit: even a mis-flagged
|
|
chromium then has no live display to open a window on.
|
|
"""
|
|
return [
|
|
"systemd-run",
|
|
"--user",
|
|
f"--unit={unit}",
|
|
"--collect",
|
|
"--quiet",
|
|
"-p",
|
|
"UnsetEnvironment=DISPLAY WAYLAND_DISPLAY",
|
|
*argv,
|
|
]
|
|
|
|
|
|
def unit_active(unit: str, runner: Runner) -> bool:
|
|
"""Is the systemd user unit running?"""
|
|
return runner(["systemctl", "--user", "is-active", "--quiet", unit]) == 0
|