docs: the Silverblue kiosk client, installed and checked over SSH
A wall display is now a target client: Fedora Silverblue running GNOME Kiosk, auto-logged in as kiosk, showing /watch fullscreen in Firefox. docs/kiosk-silverblue/ carries the idempotent installer, a read-only checker and a probe that measures what the browser actually renders, so a second client is a copy-paste and nothing about it needs a keyboard. The runbook records what the upstream tutorial leaves out: gsettings only commits from inside the session (gnome-kiosk sets DCONF_PROFILE, so a system dconf db is never read at all), window-config.ini wins by being found first and therefore silently loses the shipped notification rules, AccountsService caches Session= until its daemon restarts, and /watch survives a bare LAN IP while /mirror cannot. Verified on the reference client: cold boot back to a subscribed wall in under 60s, viewport == window == screen at 3840x2160, and the sharer's own peer connection to it reports connected. LLM-Generated: true
This commit is contained in:
Executable
+150
@@ -0,0 +1,150 @@
|
||||
#!/bin/sh
|
||||
# check-kiosk.sh — read-only report on a mirror kiosk client. Run ON THE CLIENT.
|
||||
#
|
||||
# sudo sh check-kiosk.sh
|
||||
#
|
||||
# PASS/FAIL describe this machine; WARN means "configured fine, but the thing
|
||||
# you can see on the screen will still be wrong". Exits non-zero on FAIL.
|
||||
#
|
||||
# Nothing here needs the console: every check is readable over SSH, which is
|
||||
# the whole point of a kiosk you never plug a keyboard into.
|
||||
|
||||
KIOSK_USER="${KIOSK_USER:-kiosk}"
|
||||
FAILS=0
|
||||
|
||||
ok() { printf 'PASS %s\n' "$*"; }
|
||||
bad() { printf 'FAIL %s\n' "$*"; FAILS=$((FAILS + 1)); }
|
||||
warn() { printf 'WARN %s\n' "$*"; }
|
||||
info() { printf 'info %s\n' "$*"; }
|
||||
|
||||
SUDO=""
|
||||
[ "$(id -u)" -eq 0 ] || SUDO="sudo"
|
||||
|
||||
printf '%s\n' '--- machine ---'
|
||||
if [ -f /etc/os-release ]; then
|
||||
info "$( . /etc/os-release && printf '%s %s' "$NAME" "$VERSION_ID" ) on $(uname -m)"
|
||||
fi
|
||||
if command -v rpm-ostree >/dev/null 2>&1; then
|
||||
info "booted deployment: $(rpm-ostree status 2>/dev/null | sed -n 's/^ *● *//p' | head -1)"
|
||||
info "layered: $(rpm-ostree status 2>/dev/null | sed -n 's/^ *LayeredPackages: //p' | head -1)"
|
||||
fi
|
||||
|
||||
printf '\n%s\n' '--- packages ---'
|
||||
for p in gnome-kiosk gnome-kiosk-script-session firefox; do
|
||||
if rpm -q "$p" >/dev/null 2>&1; then ok "$p installed"; else bad "$p not installed (rpm-ostree install $p)"; fi
|
||||
done
|
||||
|
||||
printf '\n%s\n' '--- user ---'
|
||||
if id -u "$KIOSK_USER" >/dev/null 2>&1; then
|
||||
HOME_DIR=$(getent passwd "$KIOSK_USER" | cut -d: -f6)
|
||||
ok "user $KIOSK_USER exists, home $HOME_DIR"
|
||||
else
|
||||
bad "user $KIOSK_USER does not exist"
|
||||
HOME_DIR=""
|
||||
fi
|
||||
|
||||
printf '\n%s\n' '--- payload ---'
|
||||
check_file() {
|
||||
path=$1 needle=$2 label=$3
|
||||
if [ -f "$path" ]; then
|
||||
if [ -n "$needle" ] && ! $SUDO grep -q "$needle" "$path" 2>/dev/null; then
|
||||
bad "$label: $path exists but has no '$needle'"
|
||||
else
|
||||
ok "$label: $path"
|
||||
fi
|
||||
else
|
||||
bad "$label: $path is missing"
|
||||
fi
|
||||
}
|
||||
URL=""
|
||||
if [ -n "$HOME_DIR" ]; then
|
||||
check_file "$HOME_DIR/.local/bin/gnome-kiosk-script" "firefox --kiosk" "kiosk script"
|
||||
if [ -f "$HOME_DIR/.config/mirror/kiosk.conf" ]; then
|
||||
URL=$($SUDO sed -n 's/^MIRROR_KIOSK_URL="\{0,1\}\([^"]*\)"\{0,1\}$/\1/p' \
|
||||
"$HOME_DIR/.config/mirror/kiosk.conf" 2>/dev/null | head -1)
|
||||
ok "kiosk URL: ${URL:-<empty>}"
|
||||
case "$URL" in
|
||||
*/watch) : ;;
|
||||
"") bad "kiosk URL is empty" ;;
|
||||
*) warn "kiosk URL is not the /watch wall: $URL" ;;
|
||||
esac
|
||||
else
|
||||
bad "kiosk config $HOME_DIR/.config/mirror/kiosk.conf is missing"
|
||||
fi
|
||||
check_file "$HOME_DIR/.config/gnome-initial-setup-done" "" "initial-setup guard"
|
||||
fi
|
||||
check_file /etc/firefox/policies/policies.json "DisableAppUpdate" "firefox policy"
|
||||
check_file /usr/local/share/gnome-kiosk/window-config.ini "org.mozilla" "kiosk window rules"
|
||||
|
||||
printf '\n%s\n' '--- session wiring ---'
|
||||
ACCT="/var/lib/AccountsService/users/$KIOSK_USER"
|
||||
if [ -f "$ACCT" ] && $SUDO grep -q '^Session=gnome-kiosk-script-wayland' "$ACCT"; then
|
||||
ok "AccountsService default session is the kiosk"
|
||||
else
|
||||
bad "AccountsService session for $KIOSK_USER is not gnome-kiosk-script-wayland"
|
||||
[ -f "$ACCT" ] && info "$($SUDO cat "$ACCT" 2>/dev/null | tr '\n' ' ')"
|
||||
fi
|
||||
if [ -f /etc/gdm/custom.conf ] &&
|
||||
$SUDO grep -qi "^AutomaticLogin=$KIOSK_USER\$" /etc/gdm/custom.conf &&
|
||||
$SUDO grep -qi "^AutomaticLoginEnable=True" /etc/gdm/custom.conf; then
|
||||
ok "gdm auto-login targets $KIOSK_USER"
|
||||
else
|
||||
bad "gdm auto-login is not enabled for $KIOSK_USER"
|
||||
fi
|
||||
|
||||
printf '\n%s\n' '--- running state ---'
|
||||
SEAT_USER=$(loginctl list-sessions --no-legend 2>/dev/null | awk '$4=="seat0"{print $3; exit}')
|
||||
if [ -z "$SEAT_USER" ]; then
|
||||
warn "nobody is logged in on seat0 (sudo systemctl restart gdm)"
|
||||
elif [ "$SEAT_USER" = "$KIOSK_USER" ]; then
|
||||
ok "seat0 is logged in as $KIOSK_USER"
|
||||
else
|
||||
bad "seat0 is logged in as '$SEAT_USER', not $KIOSK_USER (sudo systemctl restart gdm)"
|
||||
fi
|
||||
SEAT_SESSION=$(loginctl list-sessions --no-legend 2>/dev/null | awk '$4=="seat0"{print $1; exit}')
|
||||
if [ -n "$SEAT_SESSION" ]; then
|
||||
info "seat0 session $SEAT_SESSION: class=$(loginctl show-session "$SEAT_SESSION" -p Class --value 2>/dev/null) type=$(loginctl show-session "$SEAT_SESSION" -p Type --value 2>/dev/null) vt=$(loginctl show-session "$SEAT_SESSION" -p VTHint --value 2>/dev/null)"
|
||||
fi
|
||||
|
||||
if [ -n "$HOME_DIR" ]; then
|
||||
KUID=$(id -u "$KIOSK_USER")
|
||||
FF=$(pgrep -u "$KUID" -a firefox 2>/dev/null | head -1)
|
||||
if [ -z "$FF" ]; then
|
||||
warn "no firefox running as $KIOSK_USER yet"
|
||||
else
|
||||
case "$FF" in
|
||||
*--kiosk*) ok "$FF" ;;
|
||||
*) warn "firefox is running without --kiosk: $FF" ;;
|
||||
esac
|
||||
fi
|
||||
if [ -f "$HOME_DIR/.local/state/mirror-kiosk.log" ]; then
|
||||
info "last kiosk log lines:"
|
||||
$SUDO tail -5 "$HOME_DIR/.local/state/mirror-kiosk.log" 2>/dev/null | sed 's/^/ /'
|
||||
fi
|
||||
fi
|
||||
|
||||
printf '\n%s\n' '--- what the screen will show ---'
|
||||
if [ -n "$URL" ]; then
|
||||
BODY=$(curl -fsSL -m 8 "$URL" 2>/dev/null)
|
||||
if [ -z "$BODY" ]; then
|
||||
warn "$URL is not reachable from this machine right now"
|
||||
else
|
||||
CODE=$(curl -s -o /dev/null -w '%{http_code}' -m 8 "$URL" 2>/dev/null)
|
||||
if printf '%s' "$BODY" | grep -q "Watch · mirror"; then
|
||||
ok "$URL serves the mirror watch page (HTTP $CODE)"
|
||||
else
|
||||
warn "$URL answered HTTP $CODE but it is NOT the mirror watch page — the kiosk"
|
||||
warn "will happily display whatever this is. Check DNS and the nginx vhost."
|
||||
info "title: $(printf '%s' "$BODY" | sed -n 's/.*<title>\(.*\)<\/title>.*/\1/p' | head -1)"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
info "no URL configured, nothing to fetch"
|
||||
fi
|
||||
|
||||
printf '\n'
|
||||
if [ "$FAILS" -gt 0 ]; then
|
||||
printf '%s check(s) FAILED.\n' "$FAILS"
|
||||
exit 1
|
||||
fi
|
||||
printf 'All checks passed.\n'
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""dim-probe.py — measure what a headless kiosk actually renders.
|
||||
|
||||
A kiosk has no keyboard, no monitor you can read and (on Silverblue) no
|
||||
xdotool/grim to ask the compositor. So ask the browser instead: this serves one
|
||||
page that reports its own viewport and screen size back as a request, which
|
||||
lands in this server's log.
|
||||
|
||||
Run it on a machine the kiosk can reach:
|
||||
|
||||
python3 dim-probe.py # listens on 0.0.0.0:8099
|
||||
|
||||
then point the kiosk at it for a moment:
|
||||
|
||||
sudo KIOSK_URL=http://<this-host>:8099/ sh install-kiosk.sh
|
||||
sudo pkill -u kiosk firefox # the kiosk loop reloads and relaunches
|
||||
|
||||
`innerWidth == screen.width` and `fullscreenElement == null` is the signature of
|
||||
a real fullscreen window: the page occupies the whole output, so there is no
|
||||
browser chrome and no panel left over to measure. (Firefox does not report
|
||||
itself as a document.fullscreenElement when the window is fullscreen at
|
||||
startup — that API is for in-page requestFullscreen.)
|
||||
|
||||
Read-only, no dependencies, ctrl-C to stop.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import http.server
|
||||
import socketserver
|
||||
import sys
|
||||
import time
|
||||
|
||||
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8099
|
||||
|
||||
PAGE = b"""<!doctype html><html><head><meta charset="utf-8"><title>dim-probe</title></head>
|
||||
<body style="margin:0;background:#0a0f14;color:#4fd1c5;font:700 8vmin system-ui">
|
||||
<div id="t">dim-probe</div>
|
||||
<script>
|
||||
function report(tag) {
|
||||
const q = new URLSearchParams({
|
||||
tag: tag,
|
||||
inner: window.innerWidth + "x" + window.innerHeight,
|
||||
outer: window.outerWidth + "x" + window.outerHeight,
|
||||
screen: screen.width + "x" + screen.height,
|
||||
avail: screen.availWidth + "x" + screen.availHeight,
|
||||
dpr: window.devicePixelRatio,
|
||||
fsElement: document.fullscreenElement ? document.fullscreenElement.tagName : "null",
|
||||
ua: encodeURIComponent(navigator.userAgent.slice(0, 40)),
|
||||
});
|
||||
new Image().src = "/d?" + q.toString();
|
||||
}
|
||||
report("load");
|
||||
// Two more samples: if the window is still settling, the numbers move.
|
||||
setTimeout(() => report("t2s"), 2000);
|
||||
setTimeout(() => report("t6s"), 6000);
|
||||
setInterval(() => report("poll"), 10000);
|
||||
</script>
|
||||
</body></html>
|
||||
"""
|
||||
|
||||
PNG1x1 = bytes.fromhex(
|
||||
"89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489"
|
||||
"0000000d4944415478da6360000002000100fdff03fd0000000049454e44ae426082"
|
||||
)
|
||||
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self) -> None:
|
||||
if self.path.startswith("/d?"):
|
||||
query = self.path.split("?", 1)[1]
|
||||
fields = dict(kv.split("=", 1) for kv in query.split("&") if "=" in kv)
|
||||
print(
|
||||
f"{time.strftime('%H:%M:%S')} "
|
||||
f"{self.client_address[0]:<15} "
|
||||
f"{fields.get('tag', '?'):<5} "
|
||||
f"viewport={fields.get('inner', '?'):<11} "
|
||||
f"window={fields.get('outer', '?'):<11} "
|
||||
f"screen={fields.get('screen', '?'):<11} "
|
||||
f"available={fields.get('avail', '?'):<11} "
|
||||
f"fsElement={fields.get('fsElement', '?')}",
|
||||
flush=True,
|
||||
)
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "image/png")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("Content-Length", str(len(PNG1x1)))
|
||||
self.end_headers()
|
||||
self.wfile.write(PNG1x1)
|
||||
return
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("Content-Length", str(len(PAGE)))
|
||||
self.end_headers()
|
||||
self.wfile.write(PAGE)
|
||||
|
||||
def log_message(self, *args: object) -> None:
|
||||
"""Silence the default access log; the /d beacon above is the signal."""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
socketserver.TCPServer.allow_reuse_address = True
|
||||
with socketserver.ThreadingTCPServer(("0.0.0.0", PORT), Handler) as httpd:
|
||||
print(f"dim-probe on http://0.0.0.0:{PORT}/ — waiting for the kiosk…", flush=True)
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#!/bin/sh
|
||||
# ~/.local/bin/gnome-kiosk-script — the mirror kiosk application.
|
||||
#
|
||||
# Installed by docs/kiosk-silverblue/install-kiosk.sh; see that runbook.
|
||||
#
|
||||
# `gnome-kiosk-script-session` runs this file as the kiosk user inside the
|
||||
# kiosk session, and this script re-execs itself forever (the upstream example
|
||||
# does the same). That has two consequences worth designing around:
|
||||
#
|
||||
# 1. Never launch unconditionally. The loop comes back every few seconds, so
|
||||
# an unguarded launch opens a new window every few seconds.
|
||||
# 2. Never exit. `org.gnome.Kiosk.Script.service` has
|
||||
# `OnSuccess=gnome-session-shutdown.target`, so leaving this script logs
|
||||
# the kiosk out and the screen goes back to the GDM greeter.
|
||||
#
|
||||
# So: poll, (re)launch only when firefox is gone, and keep the loop alive.
|
||||
|
||||
# Source the per-client config FIRST, then derive everything from it, so the
|
||||
# payload file can stay byte-identical on every client and the kiosk.conf under
|
||||
# ~/.config/mirror is the only thing that differs. Deriving before the source
|
||||
# is a real trap: the script silently runs on the built-in default URL forever.
|
||||
CONF="${MIRROR_KIOSK_CONF:-$HOME/.config/mirror/kiosk.conf}"
|
||||
if [ -r "$CONF" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$CONF"
|
||||
fi
|
||||
|
||||
URL="${MIRROR_KIOSK_URL:-https://mirror.reeselink.com/watch}"
|
||||
PROFILE="${MIRROR_KIOSK_PROFILE:-$HOME/.mirror-kiosk}"
|
||||
LOG="${MIRROR_KIOSK_LOG:-${XDG_STATE_HOME:-$HOME/.local/state}/mirror-kiosk.log}"
|
||||
POLL="${MIRROR_KIOSK_POLL:-5}"
|
||||
|
||||
log() {
|
||||
[ -n "$LOG" ] || return 0
|
||||
# Cap the log: this script runs forever on a machine nobody logs into.
|
||||
if [ -f "$LOG" ] && [ "$(stat -c %s "$LOG" 2>/dev/null || echo 0)" -gt 1048576 ]; then
|
||||
tail -c 65536 "$LOG" >"$LOG.tmp" 2>/dev/null && mv -f "$LOG.tmp" "$LOG" 2>/dev/null
|
||||
fi
|
||||
printf '%s %s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null || echo -)" "$*" >>"$LOG" 2>/dev/null || :
|
||||
}
|
||||
|
||||
# Keep the screen on. This has to happen here, inside the session, and not at
|
||||
# install time: gsettings only commits when it can reach a D-Bus session bus,
|
||||
# so `sudo -u kiosk gsettings set ...` over SSH warns "Cannot autolaunch D-Bus
|
||||
# without X11" and silently leaves the default. It also cannot go in
|
||||
# /etc/dconf/db/local.d, because /usr/bin/gnome-kiosk sets
|
||||
# DCONF_PROFILE=gnomekiosk and that profile is `user-db:user` plus the shipped
|
||||
# compiled defaults — it never reads a system db.
|
||||
#
|
||||
# $XDG_RUNTIME_DIR is per-login, so this runs once per kiosk start and still
|
||||
# re-applies if someone changes the setting by hand.
|
||||
GSETTINGS_STAMP="${XDG_RUNTIME_DIR:-/tmp}/mirror-kiosk-gsettings.done"
|
||||
if [ ! -f "$GSETTINGS_STAMP" ]; then
|
||||
gsettings set org.gnome.desktop.session idle-delay 0 >/dev/null 2>&1
|
||||
gsettings set org.gnome.desktop.screensaver lock-enabled false >/dev/null 2>&1
|
||||
gsettings set org.gnome.desktop.interface enable-animations false >/dev/null 2>&1
|
||||
log "applied idle/lock gsettings"
|
||||
: >"$GSETTINGS_STAMP" 2>/dev/null || :
|
||||
fi
|
||||
|
||||
if pgrep -u "$(id -u)" -x firefox >/dev/null 2>&1; then
|
||||
# Up already. Nothing to do but check again in a moment.
|
||||
:
|
||||
elif command -v curl >/dev/null 2>&1 && ! curl -fsSL -m 10 -o /dev/null "$URL" 2>/dev/null; then
|
||||
# Don't park a "server not found" page on a wall display: keep polling and
|
||||
# load the page the moment the mirror server answers again. This is what
|
||||
# covers a reboot where the network has not come up yet.
|
||||
log "target not reachable yet: $URL"
|
||||
else
|
||||
mkdir -p "$PROFILE" 2>/dev/null
|
||||
log "launching: firefox --kiosk $URL"
|
||||
# --kiosk : fullscreen, no chrome, no exit affordances
|
||||
# --profile: our own dir, so the kiosk never collides with another profile
|
||||
# lock and never inherits anyone's history
|
||||
# `pgrep -x firefox` above can only be false here if a launch failed
|
||||
# outright, so a foreground run cannot wedge the loop.
|
||||
/usr/bin/firefox --kiosk --no-remote --profile "$PROFILE" "$URL" >>"$LOG" 2>&1
|
||||
rc=$?
|
||||
log "firefox exited rc=$rc"
|
||||
fi
|
||||
|
||||
sleep "$POLL"
|
||||
exec "$0" "$@"
|
||||
Executable
+156
@@ -0,0 +1,156 @@
|
||||
#!/bin/sh
|
||||
# install-kiosk.sh — turn this Fedora Silverblue box into a mirror kiosk client.
|
||||
#
|
||||
# Run ON THE CLIENT, as root, from the directory holding the payload files:
|
||||
#
|
||||
# sudo sh install-kiosk.sh
|
||||
# sudo KIOSK_URL=http://192.168.1.10:8055/watch sh install-kiosk.sh
|
||||
#
|
||||
# Idempotent: safe to re-run to repair or to change the URL. It deliberately
|
||||
# does NOT restart gdm, because that kills whatever is on the console right
|
||||
# now — it prints the command instead.
|
||||
#
|
||||
# What it configures, and why, is in ../kiosk-silverblue.md.
|
||||
|
||||
set -eu
|
||||
|
||||
KIOSK_USER="${KIOSK_USER:-kiosk}"
|
||||
KIOSK_URL="${KIOSK_URL:-https://mirror.reeselink.com/watch}"
|
||||
KIOSK_SESSION="gnome-kiosk-script-wayland"
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
NEED_REBOOT=0
|
||||
|
||||
say() { printf '%s\n' "$*"; }
|
||||
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
|
||||
need() { command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"; }
|
||||
|
||||
# Rewrite key=<value> inside [section] of an ini file, in place, preserving the
|
||||
# file's own inode so its SELinux label and mode survive.
|
||||
set_ini() {
|
||||
file=$1 section=$2 key=$3 value=$4
|
||||
awk -v sec="$section" -v key="$key" -v val="$value" '
|
||||
BEGIN { insec = 0; done = 0; hassec = 0 }
|
||||
/^[[:space:]]*\[/ {
|
||||
if (insec && !done) { print key "=" val; done = 1 }
|
||||
cur = $0
|
||||
gsub(/[[:space:]]/, "", cur); sub(/^\[/, "", cur); sub(/\]$/, "", cur)
|
||||
insec = (cur == sec)
|
||||
if (insec) hassec = 1
|
||||
print
|
||||
next
|
||||
}
|
||||
insec && $0 ~ "^[[:space:]]*" key "[[:space:]]*=" {
|
||||
if (!done) { print key "=" val; done = 1 }
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (!done) {
|
||||
if (!hassec) print "[" sec "]"
|
||||
print key "=" val
|
||||
}
|
||||
}
|
||||
' "$file" >"$file.tmp" && cat "$file.tmp" >"$file" && rm -f "$file.tmp"
|
||||
}
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "run me as root (sudo sh $0)"
|
||||
for c in rpm getent restorecon useradd sed awk grep install; do need "$c"; done
|
||||
[ -f /etc/os-release ] && grep -q '^ID=fedora' /etc/os-release ||
|
||||
say "warning: this does not look like Fedora; continuing anyway"
|
||||
|
||||
say "== 1/6 packages =="
|
||||
if rpm -q gnome-kiosk gnome-kiosk-script-session >/dev/null 2>&1; then
|
||||
say "gnome-kiosk + gnome-kiosk-script-session already present"
|
||||
else
|
||||
# Layering is the only supported way to add RPMs on Silverblue; /usr stays
|
||||
# read-only and the result is transactional.
|
||||
rpm-ostree install gnome-kiosk gnome-kiosk-script-session
|
||||
NEED_REBOOT=1
|
||||
say "layered the kiosk packages — a reboot is required before they exist"
|
||||
fi
|
||||
|
||||
say "== 2/6 kiosk user =="
|
||||
if id -u "$KIOSK_USER" >/dev/null 2>&1; then
|
||||
HOME_DIR=$(getent passwd "$KIOSK_USER" | cut -d: -f6)
|
||||
else
|
||||
useradd --home-dir "/var/home/$KIOSK_USER" --create-home --shell /bin/bash "$KIOSK_USER"
|
||||
HOME_DIR="/var/home/$KIOSK_USER"
|
||||
say "created user $KIOSK_USER (no password; auto-login does not need one)"
|
||||
fi
|
||||
[ -d "$HOME_DIR" ] || die "$KIOSK_USER has no home directory at $HOME_DIR"
|
||||
KIOSK_GROUP=$(getent passwd "$KIOSK_USER" | cut -d: -f4)
|
||||
say "user $KIOSK_USER uid=$(id -u "$KIOSK_USER") home=$HOME_DIR"
|
||||
|
||||
say "== 3/6 payload =="
|
||||
# Firefox enterprise policy: no update prompts, no account/sync, no telemetry,
|
||||
# no first-run pages. /etc is mutable on Silverblue and survives upgrades.
|
||||
install -D -m 0644 "$HERE/policies.json" /etc/firefox/policies/policies.json
|
||||
say " /etc/firefox/policies/policies.json"
|
||||
|
||||
# Window rules for gnome-kiosk. /usr/local is /var/usrlocal (writable) and is
|
||||
# searched BEFORE /usr/share, so this overrides the shipped file without
|
||||
# touching the read-only /usr.
|
||||
install -D -m 0644 "$HERE/window-config.ini" /usr/local/share/gnome-kiosk/window-config.ini
|
||||
say " /usr/local/share/gnome-kiosk/window-config.ini"
|
||||
|
||||
# `install -d` creates missing parents but leaves them owned by whoever is
|
||||
# running the installer, which is how a kiosk ends up with a root-owned
|
||||
# ~/.local and then spends its life logging "Permission denied" about
|
||||
# keyrings and ICC profiles. Create each level, then chown it explicitly.
|
||||
for d in .local .local/bin .local/state .local/share .config .config/mirror; do
|
||||
install -d -m 0755 "$HOME_DIR/$d"
|
||||
chown "$KIOSK_USER:$KIOSK_GROUP" "$HOME_DIR/$d"
|
||||
done
|
||||
install -m 0755 -o "$KIOSK_USER" -g "$KIOSK_GROUP" \
|
||||
"$HERE/gnome-kiosk-script" "$HOME_DIR/.local/bin/gnome-kiosk-script"
|
||||
say " $HOME_DIR/.local/bin/gnome-kiosk-script"
|
||||
|
||||
# The URL is the one per-client value; keep it in its own file so everything
|
||||
# else can stay byte-identical across the fleet.
|
||||
sed "s|^MIRROR_KIOSK_URL=.*|MIRROR_KIOSK_URL=\"$KIOSK_URL\"|" "$HERE/kiosk.conf" \
|
||||
| install -m 0644 -o "$KIOSK_USER" -g "$KIOSK_GROUP" /dev/stdin \
|
||||
"$HOME_DIR/.config/mirror/kiosk.conf"
|
||||
say " $HOME_DIR/.config/mirror/kiosk.conf -> $KIOSK_URL"
|
||||
|
||||
# Without this the GNOME initial-setup wizard claims the screen on first login
|
||||
# and the kiosk never starts.
|
||||
install -m 0644 -o "$KIOSK_USER" -g "$KIOSK_GROUP" /dev/null \
|
||||
"$HOME_DIR/.config/gnome-initial-setup-done"
|
||||
|
||||
say "== 4/6 default session =="
|
||||
ACCT="/var/lib/AccountsService/users/$KIOSK_USER"
|
||||
if [ -f "$ACCT" ]; then
|
||||
set_ini "$ACCT" User Session "$KIOSK_SESSION"
|
||||
else
|
||||
mkdir -p /var/lib/AccountsService/users
|
||||
printf '[User]\nSession=%s\nSystemAccount=false\n' "$KIOSK_SESSION" >"$ACCT"
|
||||
fi
|
||||
# accounts-daemon caches these files and would happily write the old value back.
|
||||
systemctl restart accounts-daemon.service 2>/dev/null || :
|
||||
say " $ACCT Session=$KIOSK_SESSION"
|
||||
|
||||
say "== 5/6 auto-login =="
|
||||
if [ ! -d /etc/gdm ]; then
|
||||
say " gdm is not installed — installing it"
|
||||
rpm-ostree install gdm
|
||||
NEED_REBOOT=1
|
||||
fi
|
||||
if [ -d /etc/gdm ]; then
|
||||
[ -f /etc/gdm/custom.conf ] || : >/etc/gdm/custom.conf
|
||||
set_ini /etc/gdm/custom.conf daemon AutomaticLoginEnable True
|
||||
set_ini /etc/gdm/custom.conf daemon AutomaticLogin "$KIOSK_USER"
|
||||
# A timed login would hand the console to the greeter on its own schedule.
|
||||
set_ini /etc/gdm/custom.conf daemon TimedLoginEnable False
|
||||
say " /etc/gdm/custom.conf: AutomaticLogin=$KIOSK_USER"
|
||||
fi
|
||||
|
||||
say "== 6/6 selinux labels =="
|
||||
restorecon -RF "$HOME_DIR/.local" "$HOME_DIR/.config" 2>/dev/null || :
|
||||
restorecon -RF /etc/firefox /usr/local/share/gnome-kiosk 2>/dev/null || :
|
||||
|
||||
say ""
|
||||
say "Installed."
|
||||
[ "$NEED_REBOOT" -eq 1 ] && say "NEXT: sudo reboot (new RPMs are only in the new deployment)"
|
||||
say "Then, to start the kiosk now (this closes anything on the console):"
|
||||
say " sudo systemctl restart gdm"
|
||||
say "Check it with: sh check-kiosk.sh"
|
||||
@@ -0,0 +1,24 @@
|
||||
# /var/home/kiosk/.config/mirror/kiosk.conf
|
||||
#
|
||||
# The single source of truth for what this kiosk displays.
|
||||
# Sourced with `.` from ~/.local/bin/gnome-kiosk-script, so: plain sh syntax,
|
||||
# no bashisms, no spaces around `=`, and no side effects in this file.
|
||||
#
|
||||
# After editing: `sudo systemctl restart gdm` (or log the kiosk user out) to
|
||||
# re-read it. The browser is restarted by the kiosk script's own loop.
|
||||
|
||||
# The wall of every live mirror. `/` is the Share/Watch chooser and needs a
|
||||
# tap to get anywhere, which is wrong for a display nobody is standing at.
|
||||
MIRROR_KIOSK_URL="https://mirror.reeselink.com/watch"
|
||||
|
||||
# Optional overrides (uncomment to change):
|
||||
|
||||
# Dedicated profile, kept out of ~/.mozilla so a wipe is one rm -rf.
|
||||
#MIRROR_KIOSK_PROFILE="$HOME/.mirror-kiosk"
|
||||
|
||||
# Where firefox's stderr goes; this is the first place to look when the kiosk
|
||||
# shows the wrong thing. Kept under 1 MiB by the script itself.
|
||||
#MIRROR_KIOSK_LOG="$HOME/.local/state/mirror-kiosk.log"
|
||||
|
||||
# Seconds between "is the browser still up?" polls.
|
||||
#MIRROR_KIOSK_POLL=5
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"policies": {
|
||||
"DisableAppUpdate": true,
|
||||
"DisableFeedbackCommands": true,
|
||||
"DisableFirefoxAccounts": true,
|
||||
"DisableFirefoxStudies": true,
|
||||
"DisableProfileRefresh": true,
|
||||
"DisableSetDesktopBackground": true,
|
||||
"DisableSync": true,
|
||||
"ExtensionUpdate": false,
|
||||
"NoDefaultBookmarks": true,
|
||||
"OfferToSaveLogins": false,
|
||||
"OverrideFirstRunPage": "",
|
||||
"Preferences": {
|
||||
"app.shield.optoutstudies.enabled": {
|
||||
"Value": false,
|
||||
"Status": "locked"
|
||||
},
|
||||
"browser.aboutConfig.showWarning": {
|
||||
"Value": false,
|
||||
"Status": "default"
|
||||
},
|
||||
"browser.migrate.interactions.bookmarks": {
|
||||
"Value": false,
|
||||
"Status": "default"
|
||||
},
|
||||
"browser.migrate.interactions.passwords": {
|
||||
"Value": false,
|
||||
"Status": "default"
|
||||
},
|
||||
"browser.sessionstore.resume_from_crash": {
|
||||
"Value": false,
|
||||
"Status": "default"
|
||||
},
|
||||
"browser.startup.page": {
|
||||
"Value": 1,
|
||||
"Status": "locked"
|
||||
},
|
||||
"datareporting.policy.dataSubmissionEnabled": {
|
||||
"Value": false,
|
||||
"Status": "locked"
|
||||
},
|
||||
"datareporting.policy.dataSubmissionPolicyBypassNotification": {
|
||||
"Value": true,
|
||||
"Status": "default"
|
||||
},
|
||||
"toolkit.telemetry.enabled": {
|
||||
"Value": false,
|
||||
"Status": "locked"
|
||||
},
|
||||
"datareporting.healthreport.uploadEnabled": {
|
||||
"Value": false,
|
||||
"Status": "locked"
|
||||
},
|
||||
"browser.urlbar.suggest.searches": {
|
||||
"Value": false,
|
||||
"Status": "locked"
|
||||
},
|
||||
"media.autoplay.blocking_policy": {
|
||||
"Value": 0,
|
||||
"Status": "locked"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
# /usr/local/share/gnome-kiosk/window-config.ini — mirror kiosk window policy.
|
||||
#
|
||||
# gnome-kiosk searches $XDG_CONFIG_HOME/gnome-kiosk/ first, then $XDG_DATA_DIRS
|
||||
# in order (/var/lib/flatpak/exports/share, /usr/local/share, /usr/share), and
|
||||
# uses the FIRST file it finds. /usr/local/share is therefore the right place
|
||||
# on Silverblue: it is mutable (it is /var/usrlocal) and it wins over the
|
||||
# shipped /usr/share/gnome-kiosk/window-config.ini without touching /usr.
|
||||
#
|
||||
# Because that shipped file is then *not* read, anything it defines has to be
|
||||
# repeated here or it is lost. The [notifications] section below is copied
|
||||
# verbatim from gnome-kiosk-50.1's shipped file for that reason.
|
||||
|
||||
# Firefox is the kiosk application: force it fullscreen even if it comes up
|
||||
# windowed (e.g. after a resume, or if --kiosk is ever dropped).
|
||||
[mirror-browser]
|
||||
match-class=org.mozilla.*
|
||||
set-fullscreen=true
|
||||
|
||||
# Copied from the shipped window-config.ini — kiosk notifications stay a
|
||||
# small overlay in the top-left instead of stealing the whole screen.
|
||||
[notifications]
|
||||
match-tag=gnome-kiosk-notification
|
||||
set-x=0
|
||||
set-y=0
|
||||
set-fullscreen=false
|
||||
set-above=true
|
||||
|
||||
# Multi-monitor: pin the browser to one output. Find the output name with
|
||||
# `wayland-info` inside the kiosk session (or `loginctl` / journal).
|
||||
#[mirror-monitor]
|
||||
#match-class=org.mozilla.*
|
||||
#set-fullscreen=true
|
||||
#set-on-monitor=eDP-1
|
||||
#lock-on-monitor=true
|
||||
Reference in New Issue
Block a user