diff --git a/install.sh b/install.sh index cfa744b..0b7e116 100755 --- a/install.sh +++ b/install.sh @@ -158,3 +158,14 @@ cat </dev/null 2>&1; then + sudo mkdir -p /etc/systemd/journald.conf.d /var/log/journal + printf '[Journal]\nStorage=persistent\nSystemMaxUse=200M\n' | sudo tee /etc/systemd/journald.conf.d/carwatch-persistent.conf >/dev/null + sudo systemctl restart systemd-journald 2>/dev/null || true + sudo journalctl --flush 2>/dev/null || true +fi diff --git a/scripts/kiosk/carwatch-kiosk.service b/scripts/kiosk/carwatch-kiosk.service new file mode 100644 index 0000000..e594487 --- /dev/null +++ b/scripts/kiosk/carwatch-kiosk.service @@ -0,0 +1,25 @@ +[Unit] +Description=CarWatch dash kiosk on the local HDMI panel (cage + Chromium) +After=carwatch-chat.service systemd-user-sessions.service +Wants=carwatch-chat.service +Conflicts=getty@tty1.service + +[Service] +Type=simple +User=%i +PAMName=login +TTYPath=/dev/tty1 +StandardInput=tty-force +StandardOutput=journal +StandardError=journal +UtmpIdentifier=tty1 +TTYReset=yes +TTYVHangup=yes +TTYVTDisallocate=yes +Environment=XDG_SESSION_TYPE=wayland +ExecStart=/usr/local/bin/carwatch-kiosk.sh +Restart=always +RestartSec=3 + +[Install] +WantedBy=graphical.target diff --git a/scripts/kiosk/carwatch-kiosk.sh b/scripts/kiosk/carwatch-kiosk.sh new file mode 100755 index 0000000..8b3b0f9 --- /dev/null +++ b/scripts/kiosk/carwatch-kiosk.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# CarWatch dash kiosk: full-screen Chromium under the cage compositor on the +# local HDMI panel, pointed at the local dash with the owner token. Started by +# carwatch-kiosk@.service on tty1. +# +# Waits for the dash before launching. If the dash never answers within the +# readiness window the script exits nonzero WITHOUT starting Chromium, so +# systemd's Restart=always keeps retrying instead of leaving a stale +# connection-error page on the car screen. +# +# Env (set by the unit or a drop-in): +# CARWATCH_CHROMIUM browser binary (default /usr/bin/chromium; the +# Ubuntu installer sets /snap/bin/chromium) +# CARWATCH_KIOSK_PROFILE Chromium profile dir; defaults depend on the binary: +# snap Chromium can only write under ~/snap/chromium, +# so the snap gets ~/snap/chromium/common/carwatch-kiosk +# CARWATCH_KIOSK_TRIES / CARWATCH_KIOSK_SLEEP readiness attempts (120) and +# seconds between them (2) +# CARWATCH_DASH_TOKEN_FILE, CARWATCH_DASH_URL +set -u +TOKEN_FILE="${CARWATCH_DASH_TOKEN_FILE:-$HOME/.carwatch/dash-token}" +DASH_URL="${CARWATCH_DASH_URL:-http://127.0.0.1:8088/dash}" +HEALTH_URL="${DASH_URL%/dash}/" +CHROMIUM="${CARWATCH_CHROMIUM:-/usr/bin/chromium}" +TRIES="${CARWATCH_KIOSK_TRIES:-120}" +SLEEP="${CARWATCH_KIOSK_SLEEP:-2}" +CAGE="${CARWATCH_CAGE:-/usr/bin/cage}" + +case "$CHROMIUM" in + /snap/*) DEFAULT_PROFILE="$HOME/snap/chromium/common/carwatch-kiosk" ;; + *) DEFAULT_PROFILE="$HOME/.carwatch/kiosk-profile" ;; +esac +PROFILE="${CARWATCH_KIOSK_PROFILE:-$DEFAULT_PROFILE}" + +ready=0 +for _ in $(seq 1 "$TRIES"); do + if curl -sf -m 2 -o /dev/null "$HEALTH_URL"; then ready=1; break; fi + sleep "$SLEEP" +done +if [ "$ready" != 1 ]; then + echo "carwatch-kiosk: dash not reachable at $HEALTH_URL after $TRIES attempts, not starting the browser" >&2 + exit 1 +fi + +TOKEN="" +[ -r "$TOKEN_FILE" ] && TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")" +URL="$DASH_URL" +[ -n "$TOKEN" ] && URL="$DASH_URL?t=$TOKEN" +mkdir -p "$PROFILE" 2>/dev/null || true +export XDG_SESSION_TYPE=wayland +export WLR_LIBINPUT_NO_DEVICES=1 +exec "$CAGE" -d -- "$CHROMIUM" \ + --kiosk --ozone-platform=wayland --no-first-run --noerrdialogs \ + --disable-infobars --disable-session-crashed-bubble --disable-pinch \ + --overscroll-history-navigation=0 --check-for-update-interval=31536000 \ + --password-store=basic --user-data-dir="$PROFILE" \ + "$URL" diff --git a/scripts/kiosk/install-kiosk-ubuntu.sh b/scripts/kiosk/install-kiosk-ubuntu.sh new file mode 100755 index 0000000..9f0b0aa --- /dev/null +++ b/scripts/kiosk/install-kiosk-ubuntu.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Ubuntu variant of install-kiosk.sh (VTA-439): Chromium comes from snap on +# Ubuntu (apt has only a shim), and a box that already runs a text console on +# tty1 (vta-console.service) keeps it on tty2 so both survive. +# Idempotent. Usage: sudo scripts/kiosk/install-kiosk-ubuntu.sh [user] +set -euo pipefail +KUSER="${1:-${SUDO_USER:-$USER}}" +HERE="$(cd -- "$(dirname -- "$0")" && pwd)" +export DEBIAN_FRONTEND=noninteractive +apt-get update -q +apt-get install -y -q --no-install-recommends cage seatd grim fonts-noto-color-emoji curl +snap list chromium >/dev/null 2>&1 || snap install chromium +install -m 755 "$HERE/carwatch-kiosk.sh" /usr/local/bin/carwatch-kiosk.sh +mkdir -p /etc/systemd/system/carwatch-kiosk@.service.d +cat > /etc/systemd/system/carwatch-kiosk@.service.d/snap-chromium.conf <<'CONF' +[Service] +Environment=CARWATCH_CHROMIUM=/snap/bin/chromium +CONF +install -m 644 "$HERE/carwatch-kiosk.service" /etc/systemd/system/carwatch-kiosk@.service +usermod -aG video,input,render "$KUSER" 2>/dev/null || true +# keep an existing tty1 text console, moved to tty2 (Ctrl+Alt+F2) +if systemctl cat vta-console.service >/dev/null 2>&1; then + mkdir -p /etc/systemd/system/vta-console.service.d + cat > /etc/systemd/system/vta-console.service.d/tty2.conf <<'CONF' +[Unit] +Conflicts= +Conflicts=getty@tty2.service +ConditionPathExists=/dev/tty2 +[Service] +ExecStartPre= +ExecStartPre=/bin/sh -c 'setterm -blank 0 -powerdown 0 > /dev/tty2 2>/dev/null || true; printf "\033[H\033[2J" > /dev/tty2' +TTYPath=/dev/tty2 +CONF +fi +systemctl daemon-reload +systemctl disable --now getty@tty1.service getty@tty2.service 2>/dev/null || true +systemctl restart vta-console.service 2>/dev/null || true +systemctl set-default graphical.target +systemctl enable --now "carwatch-kiosk@${KUSER}.service" +sleep 6 +systemctl --no-pager --lines=6 status "carwatch-kiosk@${KUSER}.service" || true +systemctl --no-pager --lines=3 status vta-console.service 2>/dev/null | head -8 || true diff --git a/scripts/kiosk/install-kiosk.sh b/scripts/kiosk/install-kiosk.sh new file mode 100755 index 0000000..820a4ad --- /dev/null +++ b/scripts/kiosk/install-kiosk.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Install the CarWatch dash kiosk on a box with an HDMI panel (Pi 5 / VTA). +# Idempotent. Usage: sudo scripts/kiosk/install-kiosk.sh [user] (default: the invoking user) +set -euo pipefail +KUSER="${1:-${SUDO_USER:-$USER}}" +HERE="$(cd -- "$(dirname -- "$0")" && pwd)" +export DEBIAN_FRONTEND=noninteractive +apt-get update -q +apt-get install -y -q --no-install-recommends cage chromium seatd grim fonts-noto-color-emoji +install -m 755 "$HERE/carwatch-kiosk.sh" /usr/local/bin/carwatch-kiosk.sh +install -m 644 "$HERE/carwatch-kiosk.service" /etc/systemd/system/carwatch-kiosk@.service +usermod -aG video,input,render "$KUSER" 2>/dev/null || true +systemctl daemon-reload +systemctl disable --now getty@tty1.service || true +systemctl set-default graphical.target +systemctl enable --now "carwatch-kiosk@${KUSER}.service" +sleep 6 +systemctl --no-pager --lines=8 status "carwatch-kiosk@${KUSER}.service" || true diff --git a/tests/test_kiosk_launcher.py b/tests/test_kiosk_launcher.py new file mode 100644 index 0000000..9ac0d9f --- /dev/null +++ b/tests/test_kiosk_launcher.py @@ -0,0 +1,59 @@ +"""The kiosk launcher must not start the browser when the dash never answers, +and must start it with the tokenized URL when the dash is up. Runs the real +bash script with stubbed curl and cage on PATH.""" +import os, stat, subprocess, tempfile, unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "kiosk" / "carwatch-kiosk.sh" + + +def _stub(dirpath: Path, name: str, body: str) -> None: + p = dirpath / name + p.write_text("#!/bin/sh\n" + body) + p.chmod(p.stat().st_mode | stat.S_IEXEC) + + +class KioskLauncherTest(unittest.TestCase): + def _run(self, curl_ok: bool): + tmp = Path(tempfile.mkdtemp()) + home = tmp / "home"; (home / ".carwatch").mkdir(parents=True) + (home / ".carwatch" / "dash-token").write_text("tok123\n") + binp = tmp / "bin"; binp.mkdir() + _stub(binp, "curl", "exit 0" if curl_ok else "exit 22") + cage_log = tmp / "cage.log" + _stub(binp, "cage", f'echo "$@" > "{cage_log}"; exit 0') + env = dict(os.environ, HOME=str(home), PATH=f"{binp}:{os.environ['PATH']}", + CARWATCH_KIOSK_TRIES="3", CARWATCH_KIOSK_SLEEP="0", + CARWATCH_CAGE=str(binp / "cage"), CARWATCH_CHROMIUM="/usr/bin/chromium") + res = subprocess.run(["bash", str(SCRIPT)], env=env, capture_output=True, text=True, timeout=30) + return res, cage_log, home + + def test_no_browser_when_dash_never_answers(self): + res, cage_log, _ = self._run(curl_ok=False) + self.assertNotEqual(res.returncode, 0, res.stderr) + self.assertFalse(cage_log.exists(), "cage/chromium was started although the dash never answered") + self.assertIn("not starting the browser", res.stderr) + + def test_browser_started_with_token_when_dash_is_up(self): + res, cage_log, home = self._run(curl_ok=True) + self.assertEqual(res.returncode, 0, res.stderr) + args = cage_log.read_text() + self.assertIn("--kiosk", args) + self.assertIn("http://127.0.0.1:8088/dash?t=tok123", args) + self.assertIn(f"--user-data-dir={home}/.carwatch/kiosk-profile", args) + + def test_snap_chromium_uses_snap_writable_profile(self): + tmp = Path(tempfile.mkdtemp()); home = tmp / "home"; (home / ".carwatch").mkdir(parents=True) + binp = tmp / "bin"; binp.mkdir(); _stub(binp, "curl", "exit 0") + cage_log = tmp / "cage.log"; _stub(binp, "cage", f'echo "$@" > "{cage_log}"; exit 0') + env = dict(os.environ, HOME=str(home), PATH=f"{binp}:{os.environ['PATH']}", + CARWATCH_KIOSK_TRIES="1", CARWATCH_KIOSK_SLEEP="0", + CARWATCH_CAGE=str(binp / "cage"), CARWATCH_CHROMIUM="/snap/bin/chromium") + res = subprocess.run(["bash", str(SCRIPT)], env=env, capture_output=True, text=True, timeout=30) + self.assertEqual(res.returncode, 0, res.stderr) + self.assertIn(f"--user-data-dir={home}/snap/chromium/common/carwatch-kiosk", cage_log.read_text()) + + +if __name__ == "__main__": + unittest.main()