#!/usr/bin/env bash # # Daynivo Kiosk OS installer. # # Turns Raspberry Pi OS Lite (Bookworm/Trixie, NetworkManager) into a Daynivo # kiosk. Also used by the image builder (inside the pi-gen chroot), so a # flashed image and a manual install are identical. # # sudo ./install.sh [--url https://…] [--name "Hal begane grond"] [--hostname hal-bg] # sudo ./install.sh --upgrade # update code, keep configuration # ./install.sh --image … # used by image/stage-voxip (no service starts) # # Existing configuration in /etc/voxip-kiosk is never overwritten. set -euo pipefail SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" MODE="install" URL="" NAME="" NEW_HOSTNAME="" FORCE=0 LIB=/usr/lib/voxip-kiosk SHARE=/usr/share/voxip-kiosk DOC=/usr/share/doc/voxip-kiosk ETC=/etc/voxip-kiosk UNITS=(voxip-kiosk-x.service voxip-kiosk-browser.service voxip-kiosk-agent.service voxip-kiosk-manager.service voxip-kiosk-healthcheck.service voxip-kiosk-healthcheck.timer voxip-network-guard.service voxip-boot-import.service voxip-kiosk-update.service voxip-kiosk-update.timer) ENABLE=(voxip-kiosk-x.service voxip-kiosk-browser.service voxip-kiosk-agent.service voxip-kiosk-manager.service voxip-kiosk-healthcheck.timer voxip-network-guard.service voxip-boot-import.service voxip-kiosk-update.timer) say() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } warn() { printf '\033[1;33mLet op:\033[0m %s\n' "$*" >&2; } die() { printf '\033[1;31mFout:\033[0m %s\n' "$*" >&2; exit 1; } usage() { sed -n '3,15p' "$0" | sed 's/^# \{0,1\}//' exit "${1:-0}" } while [[ $# -gt 0 ]]; do case "$1" in --url) URL="${2:?--url vereist een waarde}"; shift 2 ;; --name) NAME="${2:?--name vereist een waarde}"; shift 2 ;; --hostname) NEW_HOSTNAME="${2:?--hostname vereist een waarde}"; shift 2 ;; --upgrade) MODE="upgrade"; shift ;; --image) MODE="image"; shift ;; --force) FORCE=1; shift ;; -h|--help) usage 0 ;; *) warn "Onbekende optie: $1"; usage 1 ;; esac done [[ $EUID -eq 0 ]] || die "Voer dit script uit met sudo." [[ -f "$SRC/src/voxip_kiosk/config.py" ]] || die "Start install.sh vanuit de Daynivo Kiosk OS-repository." in_chroot() { if command -v systemd-detect-virt >/dev/null 2>&1 && systemd-detect-virt --chroot --quiet; then return 0 fi [[ "$(stat -c %d:%i /)" != "$(stat -c %d:%i /proc/1/root/. 2>/dev/null || echo x)" ]] } LIVE=1 if [[ $MODE == image ]] || in_chroot; then LIVE=0; fi # --- checks --------------------------------------------------------------- # In a subshell: os-release defines NAME and VERSION, which must not clobber ours. # shellcheck disable=SC1091 CODENAME="$(. /etc/os-release && echo "${VERSION_CODENAME:-}")" case "$CODENAME" in bookworm|trixie) ;; *) [[ $FORCE -eq 1 ]] || die "Niet-ondersteunde release '${CODENAME:-?}'. Ondersteund: bookworm, trixie (--force om toch door te gaan)." ;; esac if [[ $LIVE -eq 1 && $MODE == install ]]; then if ! systemctl is-enabled --quiet NetworkManager.service 2>/dev/null && systemctl is-enabled --quiet dhcpcd.service 2>/dev/null; then die "Dit systeem gebruikt dhcpcd. Daynivo Kiosk OS vereist NetworkManager (standaard sinds Bookworm). Schakel om via 'sudo raspi-config' → Advanced Options → Network Config, of gebruik de kant-en-klare image." fi fi VERSION="$(tr -d '[:space:]' < "$SRC/VERSION")" say "Daynivo Kiosk OS $VERSION installeren (modus: $MODE)" # --- packages ----------------------------------------------------------------- pkg_available() { apt-cache policy "$1" 2>/dev/null | grep -q 'Candidate: [0-9]'; } { say "Pakketten installeren" export DEBIAN_FRONTEND=noninteractive apt-get update -y CHROMIUM=chromium if pkg_available chromium-browser && [[ $CODENAME == bookworm ]]; then CHROMIUM=chromium-browser; fi PKGS=( "$CHROMIUM" xserver-xorg-core xserver-xorg-input-libinput xserver-xorg-legacy x11-xserver-utils xauth fonts-dejavu-core fonts-noto-color-emoji python3 python3-flask python3-argon2 python3-websocket gunicorn network-manager avahi-daemon iw rfkill iproute2 openssl logrotate sudo kbd plymouth ) for opt in systemd-zram-generator libraspberrypi-bin raspi-utils-core; do if pkg_available "$opt"; then PKGS+=("$opt"); fi done # The "chromium" package needs its setuid sandbox where unprivileged user namespaces are unavailable. if [[ $CHROMIUM == chromium ]] && pkg_available chromium-sandbox; then PKGS+=(chromium-sandbox); fi apt-get install -y --no-install-recommends "${PKGS[@]}" } # --- users ---------------------------------------------------------------------- say "Gebruikers aanmaken" getent group kiosk >/dev/null || groupadd --system kiosk id kiosk >/dev/null 2>&1 || useradd --system --gid kiosk --home-dir /home/kiosk --create-home \ --shell /usr/sbin/nologin --comment "Daynivo kiosk display" kiosk for g in video render input audio; do getent group "$g" >/dev/null && usermod -aG "$g" kiosk done getent group voxip >/dev/null || groupadd --system voxip id voxip >/dev/null 2>&1 || useradd --system --gid voxip --home-dir /var/lib/voxip-kiosk --no-create-home \ --shell /usr/sbin/nologin --comment "Daynivo Kiosk Manager" voxip for g in systemd-journal video; do getent group "$g" >/dev/null && usermod -aG "$g" voxip done # --- application files -------------------------------------------------------------- say "Programmabestanden installeren" install -d -m 0755 "$LIB" "$SHARE" "$DOC" rm -rf "$LIB/python.new" install -d -m 0755 "$LIB/python.new" tar -C "$SRC/src" --exclude='__pycache__' --exclude='*.pyc' -cf - voxip_kiosk | tar -C "$LIB/python.new" -xf - chown -R root:root "$LIB/python.new" chmod -R u=rwX,go=rX "$LIB/python.new" python3 -m compileall -q "$LIB/python.new" >/dev/null rm -rf "$LIB/python.old" [[ -d "$LIB/python" ]] && mv "$LIB/python" "$LIB/python.old" mv "$LIB/python.new" "$LIB/python" rm -rf "$LIB/python.old" for bin in voxip-kiosk-agent voxip-kiosk-browser voxip-kiosk-healthcheck voxip-kiosk-helper voxip-kiosk-manager voxip-kiosk-updater voxip-kiosk-x; do install -m 0755 -o root -g root "$SRC/bin/$bin" "$LIB/$bin" done install -m 0755 -o root -g root "$SRC/bin/daynivo-kiosk-ctl" /usr/local/sbin/daynivo-kiosk-ctl # The name from before the rename to Daynivo keeps working. ln -sfn daynivo-kiosk-ctl /usr/local/sbin/voxip-kiosk-ctl rm -rf "$SHARE/www" install -d -m 0755 "$SHARE/www" install -m 0644 "$SRC"/www/* "$SHARE/www/" install -m 0644 "$SRC/VERSION" "$SHARE/VERSION" install -m 0644 "$SRC/README.md" "$DOC/README.md" install -m 0644 "$SRC"/docs/*.md "$DOC/" # --- configuration ------------------------------------------------------------------- say "Configuratie" install -d -o voxip -g kiosk -m 2750 "$ETC" install -d -o voxip -g voxip -m 0750 /var/lib/voxip-kiosk install -d -o root -g root -m 0755 /var/lib/voxip-kiosk-network install -d -o voxip -g voxip -m 0750 /var/log/voxip-kiosk # Migrate from Daynivo's raspberry-pi/install-kiosk.sh: take over its URL, then disable it. LEGACY_KIOSK_UNIT=/etc/systemd/system/display-kiosk.service if [[ -f $LEGACY_KIOSK_UNIT ]]; then OLD_URL="$(grep -oE 'https://[^" ]+' "$LEGACY_KIOSK_UNIT" | tail -n1 || true)" if [[ -n $OLD_URL && -z $URL && ! -f $ETC/config.json ]]; then URL="$OLD_URL" say "URL overgenomen van de Daynivo-kioskinstallatie" fi if [[ $LIVE -eq 1 ]]; then systemctl disable --now display-kiosk.service 2>/dev/null || true; fi mv "$LEGACY_KIOSK_UNIT" "$LEGACY_KIOSK_UNIT.voxip-disabled" fi PYRUN=(python3 -I -c) "${PYRUN[@]}" ' import json, os, sys sys.path.insert(0, "/usr/lib/voxip-kiosk/python") from voxip_kiosk import config as c path, default, url, name = sys.argv[1:5] updates = {} if url: updates["url"] = url if name: updates["device_name"] = name if os.path.exists(path): # The old default device name went along with the rename to Daynivo; a chosen name stays. if "device_name" not in updates and c.load_config(c.Path(path)).get("device_name") == "VoxIP Kiosk": updates["device_name"] = "Daynivo Kiosk" if updates: c.save_config(c.Path(path), updates) print("Bestaande configuratie bijgewerkt:", ", ".join(updates)) else: print("Bestaande configuratie behouden") else: with open(default, encoding="utf-8") as fh: data = json.load(fh) data.update(c.validate(updates)) c.validate(data) c.atomic_write_json(c.Path(path), data) print("Nieuwe configuratie aangemaakt") ' "$ETC/config.json" "$SRC/config/config.default.json" "$URL" "$NAME" || die "Ongeldige --url of --name." chown voxip:kiosk "$ETC/config.json" "$ETC/config.json.bak" 2>/dev/null || true chmod 0640 "$ETC/config.json" touch "$ETC/.config.lock" && chown voxip:kiosk "$ETC/.config.lock" && chmod 0640 "$ETC/.config.lock" [[ -f $ETC/admin.json ]] && chown voxip:voxip "$ETC/admin.json" && chmod 0600 "$ETC/admin.json" # --- system integration ---------------------------------------------------------------- say "Systeemintegratie" TMP_SUDOERS="$(mktemp)" install -m 0440 "$SRC/config/sudoers" "$TMP_SUDOERS" visudo -cqf "$TMP_SUDOERS" || die "sudoers-bestand ongeldig" mv "$TMP_SUDOERS" /etc/sudoers.d/voxip-kiosk chown root:root /etc/sudoers.d/voxip-kiosk install -D -m 0644 "$SRC/config/journald.conf" /etc/systemd/journald.conf.d/voxip-kiosk.conf install -D -m 0644 "$SRC/config/watchdog.conf" /etc/systemd/system.conf.d/voxip-watchdog.conf install -D -m 0644 "$SRC/config/logrotate" /etc/logrotate.d/voxip-kiosk if [[ -f /etc/X11/Xwrapper.config && ! -f /etc/X11/Xwrapper.config.voxip-orig ]]; then cp -a /etc/X11/Xwrapper.config /etc/X11/Xwrapper.config.voxip-orig fi install -D -m 0644 "$SRC/config/Xwrapper.config" /etc/X11/Xwrapper.config for dir in /etc/chromium/policies/managed /etc/chromium-browser/policies/managed; do install -D -m 0644 "$SRC/config/chromium/voxip-kiosk.json" "$dir/voxip-kiosk.json" done ADMIN_PORT="$(python3 -I -c 'import sys; sys.path.insert(0, "/usr/lib/voxip-kiosk/python"); from voxip_kiosk.config import load_config; print(load_config(__import__("pathlib").Path(sys.argv[1]))["admin_port"])' "$ETC/config.json")" install -d -m 0755 /etc/avahi/services sed "s#8080#${ADMIN_PORT}#" "$SRC/config/avahi-voxip.service" > /etc/avahi/services/voxip-kiosk.service chmod 0644 /etc/avahi/services/voxip-kiosk.service # Swap: zram in RAM instead of a swap file on the SD card (Trixie's rpi-swap already does this). if dpkg -s systemd-zram-generator >/dev/null 2>&1 && ! dpkg -s rpi-swap >/dev/null 2>&1; then install -m 0644 "$SRC/config/zram-generator.conf" /etc/systemd/zram-generator.conf if dpkg -s dphys-swapfile >/dev/null 2>&1; then if [[ $LIVE -eq 1 ]]; then dphys-swapfile swapoff 2>/dev/null || true; fi systemctl disable dphys-swapfile.service 2>/dev/null || true rm -f /var/swap fi fi # /tmp in RAM (default on Trixie, opt-in on Bookworm). if [[ -f /usr/share/systemd/tmp.mount && ! -e /etc/systemd/system/tmp.mount ]] && ! systemctl is-enabled --quiet tmp.mount 2>/dev/null; then cp /usr/share/systemd/tmp.mount /etc/systemd/system/tmp.mount systemctl enable tmp.mount 2>/dev/null || true fi # Kernel command line: no console blanking (the kiosk runs on vt7, but keep the console # awake too), and a quiet boot behind the splash: no boot text, Raspberry logos or cursor. add_cmdline() { # add "key" or "key=value" unless the key is already present local key="${2%%=*}" grep -qE "(^| )${key//./\\.}(=[^ ]*)?( |\$)" "$1" || sed -i "1 s/\$/ $2/" "$1" } for cmdline in /boot/firmware/cmdline.txt /boot/cmdline.txt; do if [[ -f $cmdline ]]; then for opt in consoleblank=0 quiet splash plymouth.ignore-serial-consoles logo.nologo vt.global_cursor_default=0; do add_cmdline "$cmdline" "$opt" done config_txt="$(dirname "$cmdline")/config.txt" # No rainbow square from the firmware. [all] in case the file ends in a model section. if [[ -f $config_txt ]] && ! grep -q '^disable_splash=' "$config_txt"; then printf '\n[all]\ndisable_splash=1\n' >> "$config_txt" fi break fi done # Boot splash: logo on white (config/plymouth). Rebuilding the initramfs takes a while # on a Pi 3, so only when the theme changed. SPLASH_DIR=/usr/share/plymouth/themes/voxip-kiosk install -d -m 0755 "$SPLASH_DIR" install -m 0644 "$SRC"/config/plymouth/voxip-kiosk/* "$SPLASH_DIR/" if [[ ! -f /var/lib/voxip-kiosk/previous-plymouth-theme ]]; then plymouth-set-default-theme > /var/lib/voxip-kiosk/previous-plymouth-theme 2>/dev/null || true fi SPLASH_SUM="$(cat "$SPLASH_DIR"/* | sha256sum | cut -d' ' -f1)" if [[ "$(plymouth-set-default-theme 2>/dev/null)" != voxip-kiosk || "$(cat /var/lib/voxip-kiosk/splash.sha256 2>/dev/null)" != "$SPLASH_SUM" ]]; then say "Opstartscherm instellen (initramfs bijwerken, dit duurt even)" plymouth-set-default-theme voxip-kiosk if update-initramfs -u -k all >/dev/null 2>&1; then echo "$SPLASH_SUM" > /var/lib/voxip-kiosk/splash.sha256 else warn "initramfs bijwerken mislukt; het opstartscherm verschijnt mogelijk pas later tijdens het opstarten." fi fi # A desktop display manager would fight over vt7 and display :0 (Pi OS Desktop images). # uninstall.sh re-enables the ones recorded here. for dm in lightdm.service gdm3.service sddm.service; do if systemctl is-enabled --quiet "$dm" 2>/dev/null; then warn "$dm wordt uitgeschakeld (de kiosk gebruikt een eigen minimale X-sessie)." systemctl disable "$dm" 2>/dev/null || true grep -qx "$dm" /var/lib/voxip-kiosk/disabled-display-managers 2>/dev/null \ || echo "$dm" >> /var/lib/voxip-kiosk/disabled-display-managers fi if [[ $LIVE -eq 1 ]] && systemctl is-active --quiet "$dm" 2>/dev/null; then warn "$dm wordt gestopt: de desktop op het scherm sluit nu af." systemctl stop "$dm" || true fi done # --- services ------------------------------------------------------------------------------ say "Services installeren" for unit in "${UNITS[@]}"; do install -m 0644 "$SRC/systemd/$unit" "/etc/systemd/system/$unit" done if [[ $LIVE -eq 1 ]]; then systemctl daemon-reload; fi systemctl enable "${ENABLE[@]}" NetworkManager.service avahi-daemon.service >/dev/null if [[ ! -f /var/lib/voxip-kiosk/previous-default-target ]]; then systemctl get-default > /var/lib/voxip-kiosk/previous-default-target 2>/dev/null || true fi systemctl set-default multi-user.target >/dev/null if [[ -n $NEW_HOSTNAME && $LIVE -eq 0 ]]; then warn "--hostname wordt genegeerd in image-modus (gebruik TARGET_HOSTNAME van pi-gen)." elif [[ -n $NEW_HOSTNAME ]]; then "$LIB/voxip-kiosk-helper" set-hostname "$NEW_HOSTNAME" >/dev/null || die "Ongeldige hostname: $NEW_HOSTNAME" fi if [[ $LIVE -eq 0 ]]; then say "Klaar (image-modus: services starten bij de eerste boot)" exit 0 fi say "Services starten" systemctl restart systemd-journald.service || true systemctl restart avahi-daemon.service || true systemctl restart voxip-kiosk-manager.service voxip-kiosk-agent.service systemctl start voxip-kiosk-healthcheck.timer voxip-kiosk-update.timer systemctl reset-failed voxip-kiosk-x.service voxip-kiosk-browser.service 2>/dev/null || true systemctl restart voxip-kiosk-x.service systemctl restart voxip-kiosk-browser.service HOST="$(hostname)" SCHEME=http grep -q '"admin_https": true' "$ETC/config.json" && SCHEME=https echo say "Daynivo Kiosk OS $VERSION is geïnstalleerd." echo " Beheerinterface: ${SCHEME}://${HOST}.local:${ADMIN_PORT}" for ip in $(hostname -I 2>/dev/null); do [[ $ip == *:* ]] || echo " ${SCHEME}://${ip}:${ADMIN_PORT}" done if [[ ! -f $ETC/admin.json ]]; then for _ in $(seq 1 20); do [[ -f $ETC/setup-code ]] && break; sleep 0.5; done if [[ -f $ETC/setup-code ]]; then echo " Setupcode: $(cat "$ETC/setup-code") (staat ook op het scherm)" fi fi echo " Status: sudo daynivo-kiosk-ctl status"