"""test_linux_browser_flags.py — guard the Chromium keyring bypass and footprint. The keyring password prompt is the bug this file exists to prevent from coming back. It was "fixed" once before by adding ``--password-store=basic`` and ``--use-mock-keychain`` to a list (``APPLIANCE_FLAGS``) that **nothing ever referenced**, so the flags never reached the command line and the prompt persisted. A second stack-only fix (``--single-process``) also looked right on paper but crashed with a real HTTP URL. So these checks are deliberately about *what actually reaches the process*, not about what the constants say: 1. ``extra_launch_args()`` really contains the keyring + footprint flags. 2. ``launch_env()`` really strips the D-Bus session bus, so Chromium cannot reach ``gnome-keyring-daemon`` even if a flag is ever ignored. 3. Every module-level flag list is referenced by ``extra_launch_args()`` — dead flag lists are the exact failure mode that hid bug #1. 4. The spawned process is in its own session (so process-group teardown works) and its ``/proc//environ`` really lacks ``DBUS_SESSION_BUS_ADDRESS``. Run: .venv/bin/python linux/test_linux_browser_flags.py """ from __future__ import annotations import os import re import sys import time from pathlib import Path HERE = Path(__file__).resolve().parent ROOT = HERE.parent for path in (str(HERE), str(ROOT / 'src')): if path not in sys.path: sys.path.insert(0, path) import linux_browser # noqa: E402 failures: list[str] = [] checks = 0 def check(label, condition, detail=''): global checks checks += 1 if condition: print(f' PASS {label}') else: print(f' FAIL {label}' + (f' — {detail}' if detail else '')) failures.append(label) def build_adapter(**kwargs): adapter = linux_browser.LinuxChromiumAdapter( browser_path='/usr/bin/chromium', kiosk=True, **kwargs ) adapter._profile_dir = '/tmp/kiwy-flag-test/.kiosk-profile' return adapter # ── 1. Keyring flags reach the command line ────────────────────────── print('\n[1] Keyring bypass flags are applied') adapter = build_adapter() args = adapter.extra_launch_args() for flag in ('--password-store=basic', '--use-mock-keychain'): check(f'{flag} present', flag in args, 'Chromium would contact gnome-keyring and prompt for a password') check('--kiosk present', '--kiosk' in args, 'weblink would not be fullscreen') if linux_browser._detect_wayland(): check('--ozone-platform=wayland present', '--ozone-platform=wayland' in args, 'Chromium 152 aborts without an explicit Ozone platform on labwc') check( 'no ineffective --ozone-platform-hint', '--ozone-platform-hint=auto' not in args, 'the hint flag does NOT fall back to Wayland and just fails', ) # ── 2. Environment actually disconnects the keyring ────────────────── print('\n[2] launch_env() disconnects the Secret Service') env = adapter.launch_env() check('launch_env() returns an environment', env is not None, 'None means Popen inherits DBUS_SESSION_BUS_ADDRESS') if env is not None: check('DBUS_SESSION_BUS_ADDRESS removed', 'DBUS_SESSION_BUS_ADDRESS' not in env, 'Chromium could reach gnome-keyring-daemon and prompt') check('DBUS_SESSION_BUS_PID removed', 'DBUS_SESSION_BUS_PID' not in env) check('GNOME_KEYRING_CONTROL emptied', env.get('GNOME_KEYRING_CONTROL') == '', 'points the keyring client at nothing') check('CHROME_PASSWORD_STORE=basic', env.get('CHROME_PASSWORD_STORE') == 'basic') check('PATH preserved', bool(env.get('PATH')), 'browser could not exec') check('session bus is absent from the parent env to begin with', 'DBUS_SESSION_BUS_ADDRESS' in os.environ, 'precondition: this test only proves something if the player HAS a bus') check('start_new_session() is True', adapter.start_new_session() is True, 'os.killpg cannot reap Chromium children without it') # ── 3. No dead flag lists ──────────────────────────────────────────── print('\n[3] Every flag list is referenced (no dead code)') source = (HERE / 'linux_browser.py').read_text() # Names of module-level lists of flags. lists = re.findall(r'^([A-Z_]+_FLAGS) = \[', source, re.MULTILINE) check('flag lists found', len(lists) >= 5, f'only found {lists}') for name in lists: # Count references that are NOT the definition itself. uses = len(re.findall(rf'(? 0, 'a flag list nothing reads is exactly how the keyring prompt hid') # ── 4. End-to-end: the real process is detached from the bus ───────── print('\n[4] Live launch: process environment and process group') browser = linux_browser.find_linux_browser() if not browser: print(' SKIP no Chromium installed') else: os.makedirs(adapter._profile_dir, exist_ok=True) live = build_adapter() started = live.launch('about:blank', 800, 600) check('launch() returned True', started is True) proc = live._proc if started and proc is not None: try: time.sleep(1.5) if proc.poll() is not None: check('browser survived start-up', False, f'exited rc={proc.returncode}') else: check('browser survived start-up', True) # Process group: must differ from the player's own group. try: pgid = os.getpgid(proc.pid) check('browser is in its own process group', pgid == proc.pid, f'pgid={pgid} pid={proc.pid}; killpg would hit the player') except Exception as exc: check('browser is in its own process group', False, str(exc)) # /proc environ is authoritative: this is what the process sees. try: raw = Path(f'/proc/{proc.pid}/environ').read_bytes() child_env = dict( item.split('=', 1) for item in raw.decode('utf-8', 'replace').split('\x00') if '=' in item ) check('live process has no DBUS_SESSION_BUS_ADDRESS', 'DBUS_SESSION_BUS_ADDRESS' not in child_env, 'the keyring prompt can still appear') check('live process has basic password store', child_env.get('CHROME_PASSWORD_STORE') == 'basic') except Exception as exc: check('live process environment readable', False, str(exc)) finally: live.teardown() time.sleep(0.5) check('teardown() left no process behind', live._proc is None) # ── Summary ────────────────────────────────────────────────────────── print(f'\n{checks - len(failures)}/{checks} checks passed') if failures: print('\nFailed:') for name in failures: print(f' - {name}') sys.exit(1) print('All checks passed.')