3ac7f836c4
Replaces the Windows port with a Raspberry Pi / Linux implementation on Raspberry Pi OS "Trixie" (Debian 13, aarch64, Wayland/labwc). The Windows code is removed here but preserved on the Windows-Player branch. Entry point ----------- linux/run_linux.py replaces windows/run_win.py. src/main.py stays platform-neutral; all Pi-specific behaviour is injected from linux/. Five bugs that prevented the port (all measured on real hardware) ---------------------------------------------------------------- 1. Kivy's PyPI wheel bundles an SDL2 built WITHOUT the wayland driver, so no window could be created (Trixie has no X server). linux/fix_kivy_sdl2.sh symlinks the system SDL2 over the bundled filename. 2. SDL2 requires WAYLAND_DISPLAY to be *set* - the socket alone is not enough, unlike wlopm. This broke every systemd/cron/autostart launch. linux_display.ensure_session_environment() detects and exports it. 3. Kivy's Clock resolves callbacks via func.__name__; a patch assigned under a different name crashed the player ~20s after a successful start. 4. The inherited signal_screen_activity() shelled out to tvservice, xdotool and ydotool - none exist on Trixie - and mis-escaped 'wlopm --on \*', so the display blanked after 10 minutes. 5. The launchers ran src/main.py directly, bypassing every platform patch and resolving the data directory one level too high. Web links --------- - --ozone-platform-hint=auto does NOT fall back to Wayland on Chromium 152; it aborts. The platform is now chosen explicitly. - The keyring password prompt is suppressed via the ENVIRONMENT, not the flags: launch_env() strips DBUS_SESSION_BUS_ADDRESS for the child so Chromium cannot reach gnome-keyring-daemon. - Teardown kills the whole process group (needs start_new_session=True); previously it silently fell back to terminate() and orphaned children. Video normalisation ------------------- A 4K video cannot play on a Pi 4: ffpyplayer decodes in software, measured at 0.90x realtime (1080p is 3.03x). Oversized media is downscaled to 1920x1080 at sync time using the hardware h264_v4l2m2m encoder (~31s for an 18s clip), triggered by resolution only so already-playable files are untouched. src/media_state.py owns the shared on-disk contract: a .kiwy-converting marker makes the player skip the item while it is being rebuilt, then the converted file is played instead. If nothing is playable at all (a single-item playlist whose only video is converting), the player loops the intro video rather than leaving a blank screen. Also fixed ---------- - network_monitor: replaced netsh/ifconfig/dhclient with nmcli (Trixie uses NetworkManager; ifconfig and dhclient are not even installed). - Removed the Windows-only focus keeper/guardian from main.py. - main.py: duplicate SDL_AUDIODRIVER setdefault (a silent no-op); Settings "Test connection" now uses tempfile.gettempdir(). - config/app_config.json: credentials blanked so a fresh clone runs the first-run setup flow. Verification ------------ linux/test_media_state.py 18/18, test_linux_patches.py 21/21, test_linux_browser_flags.py 27/27. Verified live against a real DigiServer: image -> weblink -> image -> video with correct durations, zero leaked Chromium processes, and no throttling over a 10 minute monitored run.
181 lines
7.3 KiB
Python
181 lines
7.3 KiB
Python
"""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/<pid>/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'(?<!^){name}(?!\s*=\s*\[)', source, re.MULTILINE))
|
|
check(f'{name} is referenced', uses > 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.')
|