Files
mirror/docs/wall-watch/install-wall-watch.sh
ducoterra 8fefd83ef8 feat(kiosk): self-recover a hung wall and page when it goes quiet
An unattended wall failed in two ways on 2026-11-02. It hard-hung with the kernel
alive but userspace unable to start a process: ssh completed the handshake and
closed 6-13 ms later with zero bytes and no banner, while 62 GiB RAM and zero OOM
lines rule out memory. Separately, the service knew at 14:03Z that nothing was
watching (healthz viewers:0) and nothing was reading it. It was discovered by
walking over to the panel.

Hang recovery, two independent layers (install step 7/9, asserted by
check-kiosk.sh):

- /etc/sysctl.d/90-mirror-wall-selfheal.conf: soft lockup, hard lockup and any
  oops escalate to a panic, and kernel.panic=30 reboots instead of parking.
- /etc/systemd/system.conf.d/90-mirror-wall-watchdog.conf: PID 1 must keep kicking
  the iTCO hardware watchdog or the chipset resets the box, which is the layer that
  covers a kernel too wedged to notice. Clamped to the driver's own 30s because
  systemd kicks at a third of whatever value it is handed.

Presence alerting: docs/wall-watch/ runs on the server host, probes loopback
/healthz (exempt from the address gate) every minute, debounces three checks, pages
over ntfy and then announces the recovery. WALL_DRILL=1 labels a test page in the
title, body and priority so a drill can never be mistaken for an incident.
Verified against a fake ntfy: caller environment wins over the config file, both
transitions fire, and the debounce holds.

Firewall (install step 8/9): drop FedoraWorkstation's 1025-65535/tcp. The UDP half
is load-bearing for peer-to-peer media (mirror deliberately uses no STUN/TURN) and
stays open; the TCP half was every high port on a box whose only access control is
its address. Measured, not assumed: a live share kept flowing at 459.6 kbit/s
inbound with the TCP range closed, and tcp/8080 stopped answering from a
same-subnet host.

Also fixed: install-kiosk.sh overwrote an existing client's kiosk.conf with its
built-in default URL, so re-running it to repair a wall silently repointed it at
the production default. An existing URL now wins and KIOSK_URL= is an explicit
change.

Documented with the evidence: why gnome-remote-desktop cannot share a gnome-kiosk
session (gnome-kiosk exports no org.gnome.Mutter.* names, so grd has nothing to
capture, and this image has grdctl rather than grdc), and why no userspace RDP
would have survived the hang either.

LLM-Generated: true
2026-11-02 09:53:13 -05:00

77 lines
3.2 KiB
Bash

#!/bin/sh
# install-wall-watch.sh — install the wall-presence alert on the MIRROR SERVER host.
#
# Run ON THE SERVER, as root, from this directory:
#
# sudo sh install-wall-watch.sh
#
# Idempotent. It never overwrites /etc/mirror-wall-watch.env once it exists, so
# the ntfy token you pasted in survives a re-run.
#
# What it does, and why this exists at all: ../kiosk-silverblue.md
# ("The wall went quiet and nobody noticed").
set -eu
HERE=$(cd "$(dirname "$0")" && pwd)
CONF=/etc/mirror-wall-watch.env
say() { printf '%s\n' "$*"; }
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
[ "$(id -u)" -eq 0 ] || die "run me as root (sudo sh $0)"
for c in curl logger systemctl; do
command -v "$c" >/dev/null 2>&1 || die "missing required command: $c"
done
say "== 1/4 program =="
install -D -m 0755 "$HERE/wall-watch.sh" /usr/local/libexec/mirror-wall-watch.sh
install -d -m 0755 /usr/local/share/mirror-wall-watch
cp -a "$HERE"/. /usr/local/share/mirror-wall-watch/
rm -f /usr/local/share/mirror-wall-watch/install-wall-watch.sh
say " /usr/local/libexec/mirror-wall-watch.sh"
say " /usr/local/share/mirror-wall-watch (docs, survives reboot)"
say "== 2/4 config =="
if [ ! -f "$CONF" ]; then
install -D -m 0600 "$HERE/wall-watch.env.example" "$CONF"
say " created $CONF (0600) — NOW SET NTFY_TOKEN IN IT:"
say " sudo vi $CONF"
else
perm=$(stat -c '%a' "$CONF")
[ "$perm" = "600" ] || say " warning: $CONF is mode $perm, expected 0600 (it holds a token)"
say " kept existing $CONF"
fi
say "== 3/4 units =="
install -D -m 0644 "$HERE/mirror-wall-watch.service" /etc/systemd/system/mirror-wall-watch.service
install -D -m 0644 "$HERE/mirror-wall-watch.timer" /etc/systemd/system/mirror-wall-watch.timer
systemctl daemon-reload
systemctl enable --now mirror-wall-watch.timer
say " mirror-wall-watch.timer enabled (every minute)"
say "== 4/4 prove it =="
# One run now, so a broken config fails in front of you instead of silently in a
# timer at 03:00. The first run only records the baseline; it cannot alert, which
# is deliberate — installing the watcher must not page you about a wall that was
# already down before you had a watcher.
if systemctl start --wait mirror-wall-watch.service 2>/dev/null; then
say " first check passed:"
journalctl -u mirror-wall-watch.service --no-pager -n 1 2>/dev/null | sed 's/^/ /' || :
else
say " first check FAILED — read this before trusting the timer:"
journalctl -u mirror-wall-watch.service --no-pager -n 12 2>/dev/null | sed 's/^/ /' || :
fi
# An AVC here means SELinux disagreed with something above; say so rather than
# leaving a silently broken alert.
if command -v ausearch >/dev/null 2>&1; then
if ausearch -m avc -ts recent >/dev/null 2>&1; then
say " warning: recent AVCs found — run: sudo ausearch -m avc -ts recent"
fi
fi
say ""
say "Installed. Fire a test notification (page yourself on purpose):"
say " sudo sh -c '. /etc/mirror-wall-watch.env; WALL_DRILL=1 WALL_HEALTH_URL=https://127.0.0.1/nope WALL_THRESHOLD=1 WALL_STATE_DIR=/tmp/wall-watch-test sh /usr/local/libexec/mirror-wall-watch.sh'"
say "Then watch the timeline:"
say " journalctl -u mirror-wall-watch.service -f"