fix: make an existing classroom converge into the Plasma appliance

A real room upgraded to 0.1.45 and came up as stock Debian Plasma with
the old Waybar across the top, mako still catching notifications, and a
lock screen on an account that has no password.

The cause was one line, and it was in the lab: the harness handed the
session under test XDG_CONFIG_DIRS by hand, so every result it had ever
produced was a result about a machine somebody had already configured.
On a machine SDDM starts the session through PAM, PAM does not read
/etc/environment.d, and Debian's own plasma-workspace env.sh then set the
variable from scratch — so the classroom's settings directory was not in
the cascade at all. No layout, no lock policy, no Kiosk restrictions.

The settings now arrive the way Plasma delivers them, through a file in
/etc/xdg/plasma-workspace/env. Around that: the look and feel is
immutable on its own key so a stale profile cannot answer for it; the
palette is composed into the package, because naming a colour scheme does
not produce one and the dark bar had been coming from a technician's
hand; the desktop reset converges another desktop's appearance state and
keeps the room's own; and classroom-migrate stops, deactivates and masks
the old shell, including the D-Bus activation that brought mako back
while the panel was down — found only by killing the panel on a migrated
machine.

device.conf loses COMPOSITOR and comes off the hyprland channel, which
would otherwise have handed the room the experiment back.

The lab stops flattering the package: it restarts the machine, lets SDDM
actually autologin, and asks a fresh install and a machine upgraded from
the Hyprland package the same sixty questions. Both answer identically,
twice, and the panel still comes back from eight SIGKILLs.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
ekklesia
2026-09-10 23:06:56 -05:00
co-authored by Claude Opus 5
parent 6050144e5d
commit 926668dc9b
29 changed files with 1588 additions and 40 deletions
+236
View File
@@ -0,0 +1,236 @@
#!/bin/bash
# The upgrade that failed, reproduced on purpose.
#
# A real classroom does not arrive new, and the clean-install test was
# passing on a machine that had never been anything else. This builds a
# machine that has: the Hyprland experiment package actually installed,
# with sway, waybar and mako behind it, a Waybar activated through
# graphical-session, an autostart entry, a device.conf still asking for a
# compositor that is gone, and a Plasma profile belonging to a different
# desktop. Then it installs the classroom package over the top and asks
# whether the room converged.
#
# tools/lab/dirty-upgrade [NEW.deb] [OLD.deb]
# CHECK_ARGS=--restart tools/lab/dirty-upgrade ask twice, either side
# of a logout
#
# The container is restarted between the install and the session, because
# a room takes its upgrade and reboots.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
LAB="$ROOT/tools/lab/lab"
NAME="${LAB_NAME:-classroom-v2-lab}"
NEW="${1:-$(ls -t "$ROOT"/dist/ekklesia-classroom_*_amd64.deb | grep -v '~' | head -1)}"
OLD="${2:-$ROOT/dist/ekklesia-classroom_0.1.45~hypr3_amd64.deb}"
step() { printf '\n\033[1m== %s\033[0m\n' "$1"; }
step "a fresh machine"
"$LAB" up >/dev/null
docker exec "$NAME" sh -c '
printf "#!/bin/sh\nexit 101\n" > /usr/sbin/policy-rc.d
chmod +x /usr/sbin/policy-rc.d
apt-get update -qq >/dev/null 2>&1
# The old package schedules a provisioning run that would go to
# backports for Hyprland. The lab is not testing that.
systemctl mask ekklesia-classroom-provision.service >/dev/null 2>&1'
step "the old classroom: $(basename "$OLD")"
docker exec "$NAME" sh -c "
DEBIAN_FRONTEND=noninteractive dpkg -i '/srv/classroom/${OLD#$ROOT/}' >/dev/null 2>&1
DEBIAN_FRONTEND=noninteractive apt-get -y -qq -f install >/dev/null 2>&1
DEBIAN_FRONTEND=noninteractive dpkg --configure ekklesia-classroom >/dev/null 2>&1
true" || true
docker exec "$NAME" dpkg-query -W -f=' installed: ${Version}\n' ekklesia-classroom
step "and everything a used machine accumulates"
docker exec -i "$NAME" bash <<'DIRT'
set -e
# Waybar and mako, activated the way the real room had them. Neither
# package ships a unit in trixie, so the units themselves are part of what
# somebody added — and one of the three activation directories is the one
# the broken room actually used.
for unit in waybar mako; do
cat > "/etc/systemd/user/$unit.service" <<UNIT
[Unit]
Description=$unit (left over from the old classroom shell)
PartOf=graphical-session.target
After=graphical-session.target
[Service]
ExecStart=/usr/bin/$unit
Restart=on-failure
[Install]
WantedBy=graphical-session.target
UNIT
done
for dir in /etc/systemd/user /etc/xdg/systemd/user \
/home/classroom/.config/systemd/user; do
mkdir -p "$dir/graphical-session.target.wants"
ln -sf /etc/systemd/user/waybar.service \
"$dir/graphical-session.target.wants/waybar.service"
ln -sf /etc/systemd/user/mako.service \
"$dir/graphical-session.target.wants/mako.service"
done
# An autostart entry as well, for a bar that is started two ways.
mkdir -p /home/classroom/.config/autostart
cat > /home/classroom/.config/autostart/waybar.desktop <<'ENTRY'
[Desktop Entry]
Type=Application
Name=Waybar
Exec=waybar
ENTRY
# The room still asking for a compositor that is gone, with a broker
# password in the same file to prove the migration does not rewrite it.
sed -i 's/^CHANNEL=.*/CHANNEL=hyprland/' /etc/ekklesia-classroom/device.conf
{
echo "COMPOSITOR=hyprland"
echo "HYPR_APPEARANCE=visual"
} >> /etc/ekklesia-classroom/device.conf
sed -i 's/^MQTT_HOST=.*/MQTT_HOST=broker.example.invalid/' /etc/ekklesia-classroom/device.conf
sed -i 's/^MQTT_PASSWORD=.*/MQTT_PASSWORD=keep-me-exactly/' /etc/ekklesia-classroom/device.conf
# A Plasma profile from a different desktop: someone else's palette, in
# both of the places that outrank the package.
mkdir -p /home/classroom/.config/kdedefaults
cat > /home/classroom/.config/kdedefaults/kdeglobals <<'KDE'
[General]
ColorScheme=BreezeLight
[Icons]
Theme=breeze
[KDE]
LookAndFeelPackage=org.kde.breeze.desktop
KDE
cat > /home/classroom/.config/kdeglobals <<'KDE'
[General]
ColorScheme=BreezeLight
[Colors:Window]
BackgroundNormal=239,240,241
ForegroundNormal=35,38,41
[KDE]
LookAndFeelPackage=org.kde.breeze.desktop
KDE
# And a stock panel, already built, of the kind the broken upgrade made.
cat > /home/classroom/.config/plasma-org.kde.plasma.desktop-appletsrc <<'PANEL'
[Containments][1]
plugin=org.kde.plasma.folder
[Containments][2]
plugin=org.kde.panel
[Containments][2][Applets][3]
plugin=org.kde.plasma.kickoff
[Containments][2][Applets][4]
plugin=org.kde.plasma.pager
[Containments][2][Applets][5]
plugin=org.kde.plasma.icontasks
[Containments][2][Applets][5][Configuration][General]
launchers=applications:systemsettings.desktop,applications:org.kde.discover.desktop,preferred://browser
[Containments][2][Applets][6]
plugin=org.kde.plasma.systemtray
PANEL
# A user-level power profile that would suspend the room.
cat > /home/classroom/.config/powermanagementprofilesrc <<'POWER'
[AC][SuspendSession]
idleTime=1800000
suspendType=1
[AC][DPMSControl]
idleTime=600
POWER
chown -R classroom:classroom /home/classroom/.config
echo " dirt in place"
DIRT
step "the new classroom: $(basename "$NEW")"
docker exec "$NAME" sh -c '
systemctl unmask ekklesia-classroom-provision.service >/dev/null 2>&1 || true'
docker exec "$NAME" sh -c "
DEBIAN_FRONTEND=noninteractive dpkg -i '/srv/classroom/${NEW#$ROOT/}' 2>&1 \
| grep -E '^\[classroom-migrate\]' || true
DEBIAN_FRONTEND=noninteractive apt-get -y -qq -f install >/dev/null 2>&1
DEBIAN_FRONTEND=noninteractive dpkg --configure ekklesia-classroom >/dev/null 2>&1
rm -f /usr/sbin/policy-rc.d
true" || true
docker exec "$NAME" dpkg-query -W -f=' installed: ${Version} ${db:Status-Abbrev}\n' ekklesia-classroom
step "and the room reboots"
"$LAB" reboot
step "the session"
"$LAB" session >/dev/null
sleep 6
step "did it converge?"
converged=0
"$ROOT/tools/lab/converged-check" ${CHECK_ARGS:-} || converged=1
step "and again, installed a second time over itself"
# An update that changes nothing must change nothing. The migration runs
# again here, over a machine that is already converged.
docker exec "$NAME" sh -c "
DEBIAN_FRONTEND=noninteractive dpkg -i '/srv/classroom/${NEW#$ROOT/}' >/dev/null 2>&1
true" || true
before=$(docker exec "$NAME" sh -c 'cat /etc/ekklesia-classroom/device.conf | sha256sum')
"$LAB" plasma-down >/dev/null
"$LAB" plasma-up >/dev/null
sleep 4
after=$(docker exec "$NAME" sh -c 'cat /etc/ekklesia-classroom/device.conf | sha256sum')
[[ "$before" == "$after" ]] \
&& echo " device.conf unchanged by the second install" \
|| { echo " DEVICE.CONF CHANGED ON REINSTALL"; converged=1; }
"$ROOT/tools/lab/converged-check" || converged=1
step "and the room's own theme choice survives a logout"
# Deleting the look and feel defaults every login was measured to make
# Plasma re-apply the look and feel and throw the room's chosen palette
# away with it. This is the check that stops that coming back.
# With the session's own cascade. plasma-apply-colorscheme writes the
# chosen name into ~/.config/kdedefaults, which is in XDG_CONFIG_DIRS for
# a session and not for a bare command, so asking without it reports
# nothing and means nothing.
theme() {
local dirs
dirs=$(docker exec "$NAME" bash -c 'p=$(pgrep -x plasmashell | head -1); tr "\0" "\n" < /proc/$p/environ | grep "^XDG_CONFIG_DIRS=" | cut -d= -f2-')
docker exec -u classroom -e XDG_RUNTIME_DIR=/run/user/1000 \
-e DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \
-e WAYLAND_DISPLAY=wayland-0 -e "XDG_CONFIG_DIRS=$dirs" "$NAME" \
/usr/local/bin/classroom-theme "$1" 2>/dev/null | tail -1
}
theme light >/dev/null
"$LAB" plasma-down >/dev/null
"$LAB" plasma-up >/dev/null
sleep 4
chosen=$(theme show)
if [[ "$chosen" == EkklesiaLight ]]; then
echo " light survived the logout"
else
echo " THE ROOM'S THEME CHOICE WAS LOST: $chosen"
converged=1
fi
theme dark >/dev/null
echo " back to $(theme show)"
step "and it still recovers"
"$ROOT/tools/lab/recovery-check" || converged=1
exit "$converged"