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:
@@ -0,0 +1,270 @@
|
||||
# Kiosk client — Fedora Silverblue
|
||||
|
||||
The target client for a wall display is **Fedora Silverblue running GNOME Kiosk,
|
||||
auto-logged in as `kiosk`, showing `https://mirror.reeselink.com/watch`
|
||||
fullscreen in Firefox.** One box takes about five minutes over SSH and needs no
|
||||
keyboard, no monitor access, and no console.
|
||||
|
||||
Everything is in this directory and is idempotent — re-run it to repair a kiosk
|
||||
or to change its URL.
|
||||
|
||||
| File | Lands at | Why |
|
||||
|---|---|---|
|
||||
| `install-kiosk.sh` | run on the client as root | the whole install, one pass |
|
||||
| `check-kiosk.sh` | run on the client as root | read-only report; nothing needs the console |
|
||||
| `gnome-kiosk-script` | `~kiosk/.local/bin/gnome-kiosk-script` | the kiosk application + watchdog loop |
|
||||
| `kiosk.conf` | `~kiosk/.config/mirror/kiosk.conf` | **the one per-client value: the URL** |
|
||||
| `policies.json` | `/etc/firefox/policies/policies.json` | no updates, accounts, telemetry, first-run pages |
|
||||
| `window-config.ini` | `/usr/local/share/gnome-kiosk/window-config.ini` | force the Firefox window fullscreen |
|
||||
| `dim-probe.py` | run on any reachable host | measures what the kiosk actually renders |
|
||||
|
||||
---
|
||||
|
||||
## Add a client
|
||||
|
||||
```bash
|
||||
# 1. A vanilla Silverblue install. Enable Settings → Sharing → Remote Login.
|
||||
# Create an admin user (the first one you make) — that is your SSH account.
|
||||
|
||||
# 2. Layer the kiosk packages. This is the only step that needs a reboot.
|
||||
ssh client 'sudo rpm-ostree install gnome-kiosk gnome-kiosk-script-session && sudo reboot'
|
||||
|
||||
# 3. Wait ~60s, then install everything else.
|
||||
scp -r docs/kiosk-silverblue client:/tmp/ && ssh client \
|
||||
'cd /tmp/kiosk-silverblue && sudo sh install-kiosk.sh'
|
||||
|
||||
# 4. Hand the console to the kiosk.
|
||||
ssh client 'sudo systemctl restart gdm'
|
||||
|
||||
# 5. Prove it, without touching the machine.
|
||||
ssh client 'cd /tmp/kiosk-silverblue && sudo sh check-kiosk.sh'
|
||||
```
|
||||
|
||||
`install-kiosk.sh` creates the `kiosk` user if it does not exist, so step 2 can
|
||||
be skipped when the packages are already layered. To point a client somewhere
|
||||
other than the production wall, pass the URL:
|
||||
|
||||
```bash
|
||||
sudo KIOSK_URL=https://mirror.reeselink.com/watch sh install-kiosk.sh
|
||||
```
|
||||
|
||||
Only `kiosk.conf` differs between clients; every other file stays byte-identical
|
||||
so the fleet is auditable.
|
||||
|
||||
---
|
||||
|
||||
## The two facts that make this work
|
||||
|
||||
**Silverblue's mutable seams.** `/usr` is read-only and belongs to the OSTree
|
||||
commit, but `/etc`, `/var` and `/usr/local` (which *is* `/var/usrlocal`) are
|
||||
writable and survive upgrades. That is why the payload goes to `/etc/firefox`,
|
||||
`/usr/local/share/gnome-kiosk` and `/var/home/kiosk` and nowhere else. Never
|
||||
`ostree admin unlock` to shove files into `/usr` — the next upgrade deletes them.
|
||||
|
||||
**GNOME Kiosk's script session.** `gnome-kiosk-script-session` starts a locked
|
||||
down mutter session (no top bar, no activities, no dock) and runs
|
||||
`~/.local/bin/gnome-kiosk-script` as the kiosk application. Two consequences
|
||||
drive the design of that file:
|
||||
|
||||
* It **re-executes itself forever**, so an unguarded launch opens a new window
|
||||
every few seconds. The script checks `pgrep -u $(id -u) -x firefox` first.
|
||||
* It **must never exit**. `org.gnome.Kiosk.Script.service` carries
|
||||
`OnSuccess=gnome-session-shutdown.target`, so the script returning logs the
|
||||
kiosk out and the screen falls back to the GDM greeter. Exiting is the failure
|
||||
mode, not crashing — which is exactly why a browser that dies is a feature:
|
||||
the loop comes back and starts it again.
|
||||
|
||||
The loop also **re-sources `kiosk.conf` on every relaunch**, so changing the URL
|
||||
is:
|
||||
|
||||
```bash
|
||||
sudo sed -i 's|^MIRROR_KIOSK_URL=.*|MIRROR_KIOSK_URL="https://…/watch"|' \
|
||||
/var/home/kiosk/.config/mirror/kiosk.conf
|
||||
sudo pkill -u kiosk firefox # loop notices within 5s and relaunches
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Why each piece is where it is
|
||||
|
||||
**Auto-login — `/etc/gdm/custom.conf`**
|
||||
|
||||
```ini
|
||||
[daemon]
|
||||
AutomaticLoginEnable=True
|
||||
AutomaticLogin=kiosk
|
||||
TimedLoginEnable=False
|
||||
```
|
||||
|
||||
`TimedLoginEnable=False` matters: a timed login would hand the console to the
|
||||
greeter on its own schedule and you get a login form on your wall.
|
||||
|
||||
**Default session — `/var/lib/AccountsService/users/kiosk`**
|
||||
|
||||
```ini
|
||||
[User]
|
||||
Session=gnome-kiosk-script-wayland
|
||||
```
|
||||
|
||||
This is what GDM logs the kiosk user *into*. Without it auto-login drops the
|
||||
user into a normal GNOME session with a top bar. The installer restarts
|
||||
`accounts-daemon` afterwards, because the daemon caches these files and happily
|
||||
writes the stale value back over your edit.
|
||||
|
||||
**gsettings — applied inside the session, not at install time**
|
||||
|
||||
The screen must never blank or lock. This cannot be done from your SSH session:
|
||||
`gsettings` only commits when it can reach a D-Bus session bus, and over SSH it
|
||||
prints `Cannot autolaunch D-Bus without X11` and silently leaves the default. It
|
||||
also cannot live 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.
|
||||
|
||||
So the kiosk script applies them itself on first run each boot, stamped by a
|
||||
file in `$XDG_RUNTIME_DIR` (which is per-login, so it re-applies if someone
|
||||
changes a setting by hand):
|
||||
|
||||
```sh
|
||||
gsettings set org.gnome.desktop.session idle-delay 0
|
||||
gsettings set org.gnome.desktop.screensaver lock-enabled false
|
||||
gsettings set org.gnome.desktop.interface enable-animations false
|
||||
```
|
||||
|
||||
**Firefox window rules — `/usr/local/share/gnome-kiosk/window-config.ini`**
|
||||
|
||||
gnome-kiosk reads the **first** `window-config.ini` it finds in
|
||||
`$XDG_CONFIG_HOME/gnome-kiosk`, then `/var/lib/flatpak/exports/share`,
|
||||
`/usr/local/share`, `/usr/share`. Because ours wins, the shipped file is not
|
||||
read at all — anything it defines is lost unless repeated. Ours therefore copies
|
||||
the shipped `[notifications]` section verbatim and adds:
|
||||
|
||||
```ini
|
||||
[mirror-browser]
|
||||
match-class=org.mozilla.*
|
||||
set-fullscreen=true
|
||||
```
|
||||
|
||||
`firefox --kiosk` already goes fullscreen; this is the belt so a window that
|
||||
comes back from a resume still fills the output.
|
||||
|
||||
**Firefox policy — `/etc/firefox/policies/policies.json`**
|
||||
|
||||
No update prompts, no Firefox Account or Sync, no studies, no telemetry, no
|
||||
first-run/restore pages, no default bookmarks, no saved-password prompts.
|
||||
`/usr/lib64/firefox/distribution/` is the other documented location and it is
|
||||
inside the read-only `/usr`.
|
||||
|
||||
> Verified on this box: the file installs, is world-readable with a sane SELinux
|
||||
> label, and Firefox runs happily with it. What is *not* verified headlessly is
|
||||
> that each policy is applied — Firefox's policy engine does not initialise in
|
||||
> the `--headless --screenshot`/`--dump-dom` modes, so a probe there proves
|
||||
> nothing either way. Open `about:policies` on a real kiosk display to confirm;
|
||||
> it lists every policy Firefox read and any it rejected.
|
||||
|
||||
**Initial-setup guard — `~kiosk/.config/gnome-initial-setup-done`**
|
||||
|
||||
Without it the GNOME "welcome" wizard claims the session on first login and the
|
||||
kiosk never starts. The journal confirms the guard working:
|
||||
`gnome-initial-setup-first-login.service … skipped, unmet condition check
|
||||
ConditionPathExists=!/var/home/kiosk/.config/gnome-initial-setup-done`.
|
||||
|
||||
---
|
||||
|
||||
## Verifying a kiosk you cannot see
|
||||
|
||||
The console is not an interface. Every claim below is checkable over SSH.
|
||||
|
||||
| Claim | Evidence |
|
||||
|---|---|
|
||||
| Auto-login works | `loginctl list-sessions` → a `seat0` session whose user is `kiosk` |
|
||||
| The right session | `loginctl show-session N -p Type` → `wayland`; `pgrep -u kiosk -a gnome-kiosk` |
|
||||
| The browser is up in kiosk mode | `pgrep -u kiosk -a firefox` → `--kiosk … <url>` |
|
||||
| It restarted after a crash | `~kiosk/.local/state/mirror-kiosk.log` shows `exited rc=143` then `launching:` |
|
||||
| The page loaded | the **server's** `GET /api/streams` shows the kiosk in `viewers` and its socket in `ss -tn` |
|
||||
| Media really flows | the sharer's own page renders each viewer's `RTCPeerConnection.connectionState`; `connected` = ICE+DTLS+SRTP up |
|
||||
| It is truly fullscreen | `dim-probe.py`: `viewport == window == screen == available` |
|
||||
| The URL is serving *mirror* | `check-kiosk.sh` greps the page for `<title>Watch · mirror` |
|
||||
| It survives a power cut | `sudo systemctl reboot`, then re-run `check-kiosk.sh` |
|
||||
|
||||
Measured on the reference client, which is attached to a 4K output:
|
||||
|
||||
```
|
||||
viewport=3840x2160 window=3840x2160 screen=3840x2160 available=3840x2160
|
||||
```
|
||||
|
||||
Window equal to viewport means no browser chrome is left over; screen equal to
|
||||
available means no panel or bar took any of it.
|
||||
|
||||
After a cold boot the wall re-joins on its own — reference client back up and
|
||||
subscribed to a live mirror in under 60 seconds:
|
||||
|
||||
```
|
||||
{"mirrors": 1, "sharers": 1, "viewers": 1, "connections": 2, "watching": 1}
|
||||
Firefox · Linux | Primary Monitor | 2560 x 1600 | viewers 1
|
||||
```
|
||||
|
||||
### One trap worth knowing
|
||||
|
||||
`/watch` works over plain `http://<lan-ip>`, but **`/mirror` does not**:
|
||||
`getDisplayMedia()` needs a secure context, so the Share button is disabled
|
||||
there. Pointing a kiosk at a bare-LAN-IP mirror gives you a wall that watches
|
||||
fine and cannot share. Production is HTTPS at the edge, so both work — which is
|
||||
the main practical reason the kiosk URL should be the `https://` one and not a
|
||||
convenient IP.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause and fix |
|
||||
|---|---|
|
||||
| Greeter on screen, not the wall | `seat0` logged in as someone else — `sudo systemctl restart gdm`. GDM reads `custom.conf` at start only |
|
||||
| Kiosk logs in to a normal desktop | `Session=` empty in `/var/lib/AccountsService/users/kiosk`; re-run the installer (it restarts `accounts-daemon`) |
|
||||
| Wall shows a non-mirror page | the URL serves something else. `check-kiosk.sh` calls this out; a stale vhost/DNS looks like a working kiosk showing a login form |
|
||||
| Black screen, no browser at all | the URL is unreachable so the loop is waiting. `curl -fsSL <url>` from the client; then look in `~/.local/state/mirror-kiosk.log` |
|
||||
| A new window every few seconds | someone removed the `pgrep` guard from the kiosk script |
|
||||
| Blank on wake from idle | the gsettings did not apply (they must run inside the session) — `journalctl _UID=<kiosk uid> \| grep -i permission` |
|
||||
| `Permission denied` about keyrings/ICC | `~kiosk/.local` is not owned by kiosk. `sudo chown -R kiosk:kiosk /var/home/kiosk/.local` and re-run the installer |
|
||||
| Firefox shows a restore-session or first-run page | the policy file is missing or unreadable — `sudo cat /etc/firefox/policies/policies.json` |
|
||||
| Tile says `unreachable` | the sharer and the kiosk could not find each other over ICE. Both must be on a network where they can send UDP directly; mirror uses no STUN/TURN by design |
|
||||
| Wall shows the "not allowed" page | the kiosk's address is outside `MIRROR_ALLOW_CIDRS` — this is admission control working, see `SECURITY.md` |
|
||||
|
||||
Debugging rule for this project applies: reproduce at the lowest layer that can
|
||||
fail. `curl` the URL from the client, check `/api/streams` on the server, and
|
||||
only then reach for the browser.
|
||||
|
||||
---
|
||||
|
||||
## Deliberately not done here
|
||||
|
||||
These are real hardening steps for a kiosk in a public space; each is a
|
||||
decision, not an oversight.
|
||||
|
||||
* **The `kiosk` user has no password.** Auto-login does not need one and a
|
||||
password does not stop anyone with a keyboard. If you want to be able to log in
|
||||
as `kiosk`, set one with `sudo passwd kiosk`.
|
||||
* **TTYs are live.** Ctrl-Alt-F3 still gets a console, which is your rescue
|
||||
path. `mask`-ing `getty@tty3..6` is the usual lockdown — do it only once the
|
||||
kiosk is known good, since it removes the way back in without SSH.
|
||||
* **No GRUB password**, so anyone with physical access can boot something else.
|
||||
* **No automatic OS updates.** `rpm-ostree` is transactional and the previous
|
||||
deployment is kept, so a bad upgrade is undone with `rpm-ostree rollback` (or
|
||||
chosen from the GRUB menu if it will not boot). Unattended upgrades of a box
|
||||
nobody looks at is its own risk — decide it deliberately, via
|
||||
`rpm-ostreed-automatic.timer`.
|
||||
* **SELinux is left enforcing, unmodified.** Nothing here needed a policy tweak;
|
||||
treat a new AVC as a bug in this runbook.
|
||||
* **No reboot watchdog.** If the wall is remote enough that a hang means driving
|
||||
to it, that is an ops decision (greenboot, or a smart PDU), not a client one.
|
||||
|
||||
## Naming on the reference network
|
||||
|
||||
| Name | Address | What it is |
|
||||
|---|---|---|
|
||||
| `mirror.reeselink.com` | 10.1.1.2 | the mirror service (nginx → container), valid TLS cert |
|
||||
| `mirror-client.reeselink.com` | 10.2.1.183 | the reference kiosk client |
|
||||
| `mirror` | — | SSH alias for the kiosk client in `~/.ssh/config` |
|
||||
|
||||
The client's address is **DHCP**. A kiosk wants a reservation: the address is
|
||||
what admits it to the service (`MIRROR_ALLOW_CIDRS`, nginx `allow`/`deny`), so a
|
||||
renewal into a different range can lock a wall out by itself.
|
||||
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