Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,14 @@ cat <<DONE
carwatch-presence carwatch-netfallback carwatch-pairwatch
carwatch-update.timer (hourly self-update)
DONE

# --- persistent journal ---------------------------------------------------
# Raspberry Pi OS ships /usr/lib/systemd/journald.conf.d/40-rpi-volatile-storage.conf
# (Storage=volatile), so after a reboot the previous boot's log is gone and a
# car-side crash cannot be diagnosed. Override it; 200M cap keeps the SD card safe.
if [ -d /etc/systemd ] && command -v journalctl >/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
25 changes: 25 additions & 0 deletions scripts/kiosk/carwatch-kiosk.service
Original file line number Diff line number Diff line change
@@ -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
57 changes: 57 additions & 0 deletions scripts/kiosk/carwatch-kiosk.sh
Original file line number Diff line number Diff line change
@@ -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"
42 changes: 42 additions & 0 deletions scripts/kiosk/install-kiosk-ubuntu.sh
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions scripts/kiosk/install-kiosk.sh
Original file line number Diff line number Diff line change
@@ -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
59 changes: 59 additions & 0 deletions tests/test_kiosk_launcher.py
Original file line number Diff line number Diff line change
@@ -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()
Loading