Parent Call told rooms they had never been set up. Nothing about the rooms was wrong. Read from the church broker: …/coordinator and …/pending are retained and …/mode is absent, and the coordinator's own source says why — it publishes the retained mode only in answer to an operator writing to …/mode/set, and nobody ever has. Every classroom waited a second and a half for a message that will never exist. A missing mode is now read as the coordinator's own default rather than as an unconfigured room: the room offers the call and lets the sanctuary end answer, because that is the end that knows. A mode that really is off still refuses, in the coordinator's words, and a broker that is missing or unreachable still says which of those it is. Proved against a rig broker made to match the church's exactly, with a call landing on the sanctuary mock. The card is anchored to the bottom-left corner instead of centred, by layer shell, which is what the disc player's cover has used under KWin for months. Anchoring rather than positioning is what makes it right at 1080p and at 4K without a coordinate anywhere. It falls back to the centred card if the library is missing. Notifications are sized for a room. Every Plasma measurement is a grid unit and a grid unit is the height of the general font, so the font is the lever — 15 point instead of Breeze's 10, which takes the card from about 350x110 to about 410x145 at 100% and doubles again at 200%. Text, icon, padding and buttons move together. The chime stays off. No shell package: that path cost the panel and is not coming back. And the product name is gone from everything a teacher sees. The notification card is from Classroom, the disc player's from DVD. Co-Authored-By: Claude Opus 5 <[email protected]>
342 lines
13 KiB
Bash
Executable File
342 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
|
# Classroom v2 test lab driver.
|
|
#
|
|
# The development host is a VM with no /dev/kvm, so the desktop under test
|
|
# runs inside a systemd container instead of a virtual machine. Nothing this
|
|
# script does touches the host outside Docker.
|
|
#
|
|
# The session is nested: an outer headless sway exists only to give the
|
|
# desktop under test a display that grim can capture and that sway's IPC can
|
|
# aim a real pointer at. The outer compositor is lab equipment, not part of
|
|
# the classroom design.
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
NAME="${LAB_NAME:-classroom-v2-lab}"
|
|
IMAGE="${LAB_IMAGE:-classroom-v2-lab:plasma}"
|
|
DOCKERFILE="${LAB_DOCKERFILE:-tools/lab/Dockerfile.plasma}"
|
|
SHOTS="${LAB_SHOTS:-$ROOT/docs/screenshots}"
|
|
|
|
OUTER=wayland-1
|
|
|
|
session_env() {
|
|
docker exec -u classroom \
|
|
-e XDG_RUNTIME_DIR=/run/user/1000 \
|
|
-e HOME=/home/classroom \
|
|
-e LANG=C.UTF-8 \
|
|
-e DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \
|
|
"$@"
|
|
}
|
|
|
|
# Talk to the outer lab compositor: grim, swaymsg.
|
|
outer() {
|
|
local sock
|
|
sock=$(docker exec "$NAME" sh -c 'ls /run/user/1000/sway-ipc.* 2>/dev/null | head -1')
|
|
session_env -e WAYLAND_DISPLAY="$OUTER" -e SWAYSOCK="$sock" "$NAME" "$@"
|
|
}
|
|
|
|
# NOTE ON XDG_CONFIG_DIRS.
|
|
#
|
|
# This harness used to hand the session
|
|
# XDG_CONFIG_DIRS=/etc/ekklesia-classroom/xdg:/etc/xdg, and every result it
|
|
# produced was therefore a result about a machine somebody had already
|
|
# configured by hand. A real session is started by SDDM through PAM, which
|
|
# does not read /etc/environment.d, and the classroom's settings were not
|
|
# in the cascade at all: the first real upgrade came up as stock Debian
|
|
# Plasma with a lock screen. The variable is deliberately not set here any
|
|
# more. The package has to put it there, the way it has to on a machine.
|
|
|
|
# Talk to the Plasma session under test.
|
|
inner() {
|
|
session_env -e WAYLAND_DISPLAY=wayland-0 -e XDG_CURRENT_DESKTOP=KDE \
|
|
-e QT_QPA_PLATFORM=wayland "$NAME" "$@"
|
|
}
|
|
|
|
case "${1:-}" in
|
|
build)
|
|
shift
|
|
exec docker build -f "$ROOT/$DOCKERFILE" -t "$IMAGE" "$@" "$ROOT"
|
|
;;
|
|
|
|
up)
|
|
docker rm -f "$NAME" >/dev/null 2>&1 || true
|
|
docker run -d --name "$NAME" \
|
|
--privileged --cgroupns=private \
|
|
--tmpfs /run --tmpfs /run/lock \
|
|
--shm-size=1g \
|
|
-v "$ROOT:/srv/classroom:ro" \
|
|
"$IMAGE" >/dev/null
|
|
for _ in $(seq 1 90); do
|
|
state=$(docker exec "$NAME" systemctl is-system-running 2>/dev/null || true)
|
|
[[ "$state" == running || "$state" == degraded ]] && break
|
|
sleep 1
|
|
done
|
|
for _ in $(seq 1 30); do
|
|
docker exec "$NAME" test -S /run/user/1000/bus && break
|
|
sleep 1
|
|
done
|
|
# startplasma looks for `qdbus`; Debian ships it as qdbus6.
|
|
docker exec "$NAME" ln -sf /usr/bin/qdbus6 /usr/local/bin/qdbus
|
|
|
|
# The church's own networks, refused from inside the container.
|
|
#
|
|
# This is not theoretical. The moment the package began defaulting
|
|
# MQTT_HOST to the church broker, the lab connected to it: a test
|
|
# machine appearing on the real broker under a container id. Nothing
|
|
# was left behind — the broker held no retained status afterwards,
|
|
# which was checked — but the lab had no business being there at all,
|
|
# and the Parent Call coordinator is on the same networks.
|
|
#
|
|
# Container-local: this netns belongs to the container alone and
|
|
# nothing on the host or the church network is changed by it. Debian's
|
|
# own mirrors are untouched, so installs still work.
|
|
# A blackhole route rather than a firewall rule: the lab image has no
|
|
# iptables, and the rig's guard had been quietly doing nothing because
|
|
# of it. Routes need no netfilter modules and cannot silently fail.
|
|
docker exec "$NAME" sh -c '
|
|
for net in 10.30.0.0/16 10.40.0.0/16; do
|
|
ip route replace blackhole "$net" || exit 1
|
|
done' || echo " WARNING: could not fence the church networks"
|
|
|
|
# And checked with bash, not sh. The probe uses /dev/tcp, which is a
|
|
# bash feature: under dash it fails whatever the network is doing, so
|
|
# the check passed on a lab that could reach the broker perfectly
|
|
# well. Measured.
|
|
for target in 10.40.0.2/1883 10.30.0.2/1025; do
|
|
if docker exec "$NAME" timeout 4 bash -c "exec 3<>/dev/tcp/${target}" 2>/dev/null; then
|
|
echo "REFUSING TO CONTINUE: ${target} is reachable from the lab" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
echo " church networks fenced out of the lab"
|
|
echo "lab $NAME is up ($(docker exec "$NAME" systemctl is-system-running 2>/dev/null))"
|
|
;;
|
|
|
|
down)
|
|
docker rm -f "$NAME" >/dev/null 2>&1 || true
|
|
echo "lab $NAME is down"
|
|
;;
|
|
|
|
reboot)
|
|
# As close to a room being switched off and on as a container gets:
|
|
# the same filesystem, a new PID 1, a new user manager, and every
|
|
# generator and drop-in read again from scratch. The migration work
|
|
# needs this, because half of what an upgrade changes is only read at
|
|
# boot.
|
|
docker restart "$NAME" >/dev/null
|
|
for _ in $(seq 1 90); do
|
|
state=$(docker exec "$NAME" systemctl is-system-running 2>/dev/null || true)
|
|
[[ "$state" == running || "$state" == degraded ]] && break
|
|
sleep 1
|
|
done
|
|
for _ in $(seq 1 30); do
|
|
docker exec "$NAME" test -S /run/user/1000/bus && break
|
|
sleep 1
|
|
done
|
|
docker exec "$NAME" ln -sf /usr/bin/qdbus6 /usr/local/bin/qdbus
|
|
|
|
# The fence again. Routes do not survive a container restart, and the
|
|
# lab reached the church broker through exactly that gap: four rooms
|
|
# named after container ids on the real broker, from sessions that
|
|
# had been fenced when they started and were not after a reboot.
|
|
docker exec "$NAME" sh -c '
|
|
for net in 10.30.0.0/16 10.40.0.0/16; do
|
|
ip route replace blackhole "$net" || exit 1
|
|
done' || echo " WARNING: could not fence the church networks"
|
|
|
|
for target in 10.40.0.2/1883 10.30.0.2/1025; do
|
|
if docker exec "$NAME" timeout 4 bash -c "exec 3<>/dev/tcp/${target}" 2>/dev/null; then
|
|
echo "REFUSING TO CONTINUE: ${target} is reachable from the lab" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# SDDM really does autologin here: it authenticates the classroom
|
|
# through PAM and launches the Plasma session, and then has no display
|
|
# hardware to draw it on. That is worth having — it is the real login
|
|
# path, not a reading of a configuration file — so the attempt is left
|
|
# to happen and its evidence stays in the journal. It is then stopped,
|
|
# because the harness runs its own session on the nested compositor
|
|
# and two sessions cannot both own the keyboard.
|
|
for _ in $(seq 1 30); do
|
|
docker exec "$NAME" journalctl -u sddm --no-pager 2>/dev/null \
|
|
| grep -q "Session started" && break
|
|
sleep 1
|
|
done
|
|
docker exec "$NAME" systemctl stop sddm >/dev/null 2>&1 || true
|
|
docker exec "$NAME" pkill -f sddm-helper >/dev/null 2>&1 || true
|
|
# -f, not -x. "startplasma-wayland" is longer than the fifteen
|
|
# characters a process name has, so -x silently matches nothing and
|
|
# SDDM's session was left winding down underneath the harness's own:
|
|
# it tore down graphical-session.target sixteen seconds later and took
|
|
# the panel with it, which cost an afternoon to understand.
|
|
docker exec "$NAME" pkill -f startplasma-wayland >/dev/null 2>&1 || true
|
|
docker exec "$NAME" pkill -f plasma-dbus-run-session >/dev/null 2>&1 || true
|
|
for _ in $(seq 1 20); do
|
|
state=$(docker exec -u classroom -e XDG_RUNTIME_DIR=/run/user/1000 "$NAME" \
|
|
systemctl --user is-active graphical-session.target 2>/dev/null || true)
|
|
[[ "$state" != active ]] && break
|
|
sleep 1
|
|
done
|
|
sleep 2
|
|
echo "lab $NAME rebooted ($(docker exec "$NAME" systemctl is-system-running 2>/dev/null || true))"
|
|
;;
|
|
|
|
outer-up)
|
|
# A headless compositor has no input devices, and a wl_seat with no
|
|
# pointer capability never gives the session under test a pointer to
|
|
# click with — sway's cursor commands then report success and move
|
|
# nothing. So the lab runs wlroots' libinput backend as well, over the
|
|
# devices the privileged container inherits, with seatd standing in for
|
|
# the login session it does not have. Clicks in this experiment are
|
|
# real pointer events arriving through libinput.
|
|
docker exec "$NAME" sh -c 'pgrep -x seatd >/dev/null || \
|
|
(SEATD_VTBOUND=0 setsid seatd -g input > /tmp/seatd.log 2>&1 &)'
|
|
docker exec "$NAME" usermod -aG input classroom 2>/dev/null || true
|
|
sleep 2
|
|
session_env \
|
|
-e WLR_BACKENDS=headless,libinput -e LIBSEAT_BACKEND=seatd \
|
|
-e WLR_RENDERER=pixman -e LIBGL_ALWAYS_SOFTWARE=1 \
|
|
-d "$NAME" sh -c 'exec sway -c /srv/classroom/tools/lab/outer-sway.conf \
|
|
> /tmp/outer-sway.log 2>&1'
|
|
for _ in $(seq 1 30); do
|
|
docker exec "$NAME" test -S "/run/user/1000/$OUTER" && break
|
|
sleep 1
|
|
done
|
|
echo "outer compositor on $OUTER"
|
|
;;
|
|
|
|
plasma-up)
|
|
session_env \
|
|
-e WAYLAND_DISPLAY="$OUTER" -e XDG_SESSION_TYPE=wayland \
|
|
-e XDG_CURRENT_DESKTOP=KDE -e LIBGL_ALWAYS_SOFTWARE=1 \
|
|
-e QT_LOGGING_RULES="kwin_scripting.debug=true" \
|
|
-d "$NAME" sh -c 'exec startplasma-wayland > /tmp/plasma.log 2>&1'
|
|
for _ in $(seq 1 60); do
|
|
docker exec "$NAME" pgrep -x plasmashell >/dev/null 2>&1 && break
|
|
sleep 1
|
|
done
|
|
sleep 5
|
|
echo "plasma session up"
|
|
;;
|
|
|
|
plasma-down)
|
|
# A logout, not a killing. plasmashell and kwin are both services of
|
|
# the user manager, and killing them only makes it start them again —
|
|
# the compositor came straight back holding wayland-0 and the next
|
|
# session could not start at all. The workspace target has to go down
|
|
# first, and this is what a real logout does too.
|
|
session_env "$NAME" systemctl --user stop plasma-workspace.target \
|
|
>/dev/null 2>&1 || true
|
|
docker exec "$NAME" pkill -u classroom -f startplasma-wayland || true
|
|
session_env "$NAME" systemctl --user stop graphical-session.target \
|
|
>/dev/null 2>&1 || true
|
|
for _ in $(seq 1 20); do
|
|
docker exec "$NAME" pgrep -x kwin_wayland >/dev/null 2>&1 || break
|
|
sleep 1
|
|
done
|
|
docker exec "$NAME" pkill -u classroom -x kwin_wayland >/dev/null 2>&1 || true
|
|
docker exec "$NAME" pkill -u classroom -x plasmashell >/dev/null 2>&1 || true
|
|
sleep 2
|
|
echo "plasma session down"
|
|
;;
|
|
|
|
session)
|
|
"$0" outer-up
|
|
"$0" plasma-up
|
|
;;
|
|
|
|
shot)
|
|
# shot NAME [GEOMETRY] — geometry as grim wants it, e.g. "0,980 1920x100"
|
|
name="${2:?usage: lab shot NAME}"
|
|
if [[ -n "${3:-}" ]]; then
|
|
outer grim -g "$3" /tmp/lab-shot.png >/dev/null
|
|
else
|
|
outer grim /tmp/lab-shot.png >/dev/null
|
|
fi
|
|
mkdir -p "$SHOTS"
|
|
docker exec "$NAME" cat /tmp/lab-shot.png > "$SHOTS/$name.png"
|
|
echo "$SHOTS/$name.png"
|
|
;;
|
|
|
|
click)
|
|
# Absolute, through sway's own seat. This only works because the seat
|
|
# has a pointer capability at all, which is what the uinput device in
|
|
# `outer-up` is for.
|
|
outer swaymsg seat - cursor set "$2" "$3" >/dev/null
|
|
sleep 0.4
|
|
outer swaymsg seat - cursor press button1 >/dev/null
|
|
sleep 0.15
|
|
outer swaymsg seat - cursor release button1 >/dev/null
|
|
sleep 0.5
|
|
;;
|
|
|
|
rclick)
|
|
outer swaymsg seat - cursor set "$2" "$3" >/dev/null
|
|
sleep 0.4
|
|
outer swaymsg seat - cursor press button3 >/dev/null
|
|
sleep 0.15
|
|
outer swaymsg seat - cursor release button3 >/dev/null
|
|
sleep 0.5
|
|
;;
|
|
|
|
move)
|
|
outer swaymsg seat - cursor set "$2" "$3" >/dev/null
|
|
;;
|
|
|
|
drag)
|
|
outer swaymsg seat - cursor set "$2" "$3" >/dev/null
|
|
sleep 0.3
|
|
outer swaymsg seat - cursor press button1 >/dev/null
|
|
sleep 0.3
|
|
outer swaymsg seat - cursor set "$4" "$5" >/dev/null
|
|
sleep 0.3
|
|
outer swaymsg seat - cursor release button1 >/dev/null
|
|
sleep 0.4
|
|
;;
|
|
|
|
key)
|
|
shift
|
|
outer wtype "$@"
|
|
;;
|
|
|
|
sh)
|
|
shift
|
|
exec docker exec -it "$NAME" bash "$@"
|
|
;;
|
|
|
|
run) shift; session_env "$NAME" "$@" ;;
|
|
irun) shift; session_env -it "$NAME" "$@" ;;
|
|
inner) shift; inner "$@" ;;
|
|
inner-d) shift; session_env -d -e WAYLAND_DISPLAY=wayland-0 -e XDG_CURRENT_DESKTOP=KDE \
|
|
-e QT_QPA_PLATFORM=wayland "$NAME" "$@" ;;
|
|
inner-it) shift; session_env -it -e WAYLAND_DISPLAY=wayland-0 -e XDG_CURRENT_DESKTOP=KDE "$NAME" "$@" ;;
|
|
outer) shift; outer "$@" ;;
|
|
|
|
*)
|
|
cat <<USAGE
|
|
usage: tools/lab/lab COMMAND
|
|
|
|
build build the lab image
|
|
up start the lab container (systemd inside)
|
|
down remove it
|
|
reboot restart the machine, let SDDM autologin, then stand down
|
|
session outer compositor + a Plasma session on top of it
|
|
outer-up just the outer compositor
|
|
plasma-up just the Plasma session
|
|
plasma-down stop the Plasma session
|
|
shot NAME capture the screen to docs/screenshots/NAME.png
|
|
click X Y press the pointer at a screen position
|
|
drag X1 Y1 X2 Y2 drag from one position to another
|
|
move X Y move the pointer
|
|
key ARGS... wtype, aimed at the session
|
|
inner CMD... run CMD as a client of the Plasma session
|
|
inner-d CMD... the same, detached
|
|
outer CMD... run CMD against the lab compositor
|
|
run CMD... run CMD as the classroom user, no display
|
|
sh root shell in the lab
|
|
USAGE
|
|
exit 1
|
|
;;
|
|
esac
|