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:
co-authored by
Claude Opus 5
parent
6050144e5d
commit
926668dc9b
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compose the classroom's kdeglobals, colours included.
|
||||
|
||||
The panel came up light on a machine that had never had a colour scheme
|
||||
applied to it, and dark on one that had. That is the whole bug in one
|
||||
sentence: `ColorScheme=EkklesiaDark` is a label. KConfig reads the actual
|
||||
colours out of [Colors:*] groups in kdeglobals, and nothing was putting
|
||||
them there except a technician running classroom-theme once, by hand, on
|
||||
a machine that then looked right for reasons the package did not own.
|
||||
|
||||
So the package ships the expanded palette in its own kdeglobals, at the
|
||||
bottom of the cascade, where a room with no user configuration at all
|
||||
finds it. One source of truth stays EkklesiaDark.colors: this composes,
|
||||
it does not duplicate.
|
||||
|
||||
classroom-theme still works. plasma-apply-colorscheme writes the chosen
|
||||
palette into the user's own kdeglobals, which sits ahead of this file, and
|
||||
classroom-desktop-reset keeps a user palette whose name is an Ekklesia one.
|
||||
|
||||
tools/build-kdeglobals > somewhere/kdeglobals
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
POLICY = ROOT / "config/plasma/xdg/kdeglobals"
|
||||
SCHEME = ROOT / "config/plasma/colors/EkklesiaDark.colors"
|
||||
|
||||
# Name belongs to the scheme file, not to a session's configuration, and
|
||||
# ColorScheme is already stated in the policy file above.
|
||||
SKIP_IN_GENERAL = {"Name", "ColorScheme"}
|
||||
|
||||
|
||||
def groups(text):
|
||||
"""Yield (header, [lines]) for each group in a KConfig file."""
|
||||
header = None
|
||||
body = []
|
||||
|
||||
for line in text.splitlines():
|
||||
if line.startswith("["):
|
||||
if header is not None:
|
||||
yield header, body
|
||||
header, body = line, []
|
||||
elif header is not None:
|
||||
body.append(line)
|
||||
|
||||
if header is not None:
|
||||
yield header, body
|
||||
|
||||
|
||||
def policy_with(extra_general):
|
||||
"""The policy file, with the scheme's stray [General] keys folded in.
|
||||
|
||||
Two [General] headers in one file is asking a parser a question it
|
||||
should not be asked, so the keys go into the group that is already
|
||||
there.
|
||||
"""
|
||||
lines = POLICY.read_text().rstrip("\n").splitlines()
|
||||
out = []
|
||||
inside = False
|
||||
|
||||
for line in lines:
|
||||
if line.startswith("["):
|
||||
if inside:
|
||||
out.extend(extra_general)
|
||||
inside = False
|
||||
|
||||
inside = line == "[General]"
|
||||
|
||||
out.append(line)
|
||||
|
||||
if inside:
|
||||
out.extend(extra_general)
|
||||
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def main():
|
||||
extra_general = []
|
||||
|
||||
for header, body in groups(SCHEME.read_text()):
|
||||
if header != "[General]":
|
||||
continue
|
||||
|
||||
extra_general = [
|
||||
line
|
||||
for line in body
|
||||
if line.strip()
|
||||
and not line.startswith("#")
|
||||
and line.split("=", 1)[0].strip() not in SKIP_IN_GENERAL
|
||||
]
|
||||
|
||||
out = [policy_with(extra_general), ""]
|
||||
|
||||
out.append(
|
||||
"# ------------------------------------------------------------------\n"
|
||||
"# Generated by tools/build-kdeglobals from EkklesiaDark.colors.\n"
|
||||
"# Do not edit here: edit the scheme file and rebuild the package.\n"
|
||||
"#\n"
|
||||
"# These are the colours themselves. Without them a room with no user\n"
|
||||
"# configuration gets KDE's built-in light palette however the scheme\n"
|
||||
"# above is named.\n"
|
||||
"# ------------------------------------------------------------------"
|
||||
)
|
||||
|
||||
for header, body in groups(SCHEME.read_text()):
|
||||
lines = [line for line in body if line.strip() and not line.startswith("#")]
|
||||
|
||||
if header == "[General]":
|
||||
# Already folded into the policy file's own [General] above.
|
||||
continue
|
||||
|
||||
if not lines:
|
||||
continue
|
||||
|
||||
out.append("")
|
||||
out.append(header)
|
||||
out.extend(lines)
|
||||
|
||||
sys.stdout.write("\n".join(out) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+30
-1
@@ -206,17 +206,32 @@ install -Dm644 \
|
||||
|
||||
# Not /etc/xdg: plasma-workspace already owns files by these names there.
|
||||
# The classroom's directory goes in front of it in XDG_CONFIG_DIRS instead.
|
||||
for setting in kdeglobals kwinrc kwinrulesrc kscreenlockerrc ksmserverrc \
|
||||
for setting in kwinrc kwinrulesrc kscreenlockerrc ksmserverrc \
|
||||
krunnerrc plasmanotifyrc powermanagementprofilesrc; do
|
||||
install -Dm644 \
|
||||
"$ROOT/config/plasma/xdg/${setting}" \
|
||||
"$STAGE/etc/ekklesia-classroom/xdg/${setting}"
|
||||
done
|
||||
|
||||
# kdeglobals is composed rather than copied: the session policy plus the
|
||||
# dark palette itself. Naming a colour scheme does not produce one, and a
|
||||
# room whose user profile had never had a scheme applied came up light.
|
||||
install -d "$STAGE/etc/ekklesia-classroom/xdg"
|
||||
"$ROOT/tools/build-kdeglobals" > "$STAGE/etc/ekklesia-classroom/xdg/kdeglobals"
|
||||
chmod 0644 "$STAGE/etc/ekklesia-classroom/xdg/kdeglobals"
|
||||
|
||||
install -Dm644 \
|
||||
"$ROOT/config/environment/90-ekklesia-classroom.conf" \
|
||||
"$STAGE/etc/environment.d/90-ekklesia-classroom.conf"
|
||||
|
||||
# The one that actually reaches a session started by a display manager.
|
||||
# startplasma sources this directory; PAM does not read environment.d, so
|
||||
# without this file none of the settings above are in the cascade at all
|
||||
# and the room comes up as stock Plasma. Measured, on a real machine.
|
||||
install -Dm644 \
|
||||
"$ROOT/config/plasma/env/zz-ekklesia-classroom.sh" \
|
||||
"$STAGE/etc/xdg/plasma-workspace/env/zz-ekklesia-classroom.sh"
|
||||
|
||||
# The power button does nothing. Two layers, because while a session is
|
||||
# running PowerDevil holds logind's inhibitor and would overrule it.
|
||||
install -Dm644 \
|
||||
@@ -260,6 +275,14 @@ install -Dm644 \
|
||||
"$ROOT/config/plasma/solid/ekklesia-dvd.desktop" \
|
||||
"$STAGE/usr/share/solid/actions/ekklesia-dvd.desktop"
|
||||
|
||||
# Shadows the old notification daemon's D-Bus activation. Deactivating its
|
||||
# systemd unit is not enough: D-Bus starts it on demand from a file
|
||||
# mako-notifier owns, and the crash-recovery test on a migrated machine
|
||||
# caught it doing exactly that while the panel was down.
|
||||
install -Dm644 \
|
||||
"$ROOT/config/dbus/fr.emersion.mako.service" \
|
||||
"$STAGE/usr/local/share/dbus-1/services/fr.emersion.mako.service"
|
||||
|
||||
# Two marks the classroom draws itself, because nothing in Breeze says
|
||||
# either thing: a lesson, and fetching a child's grown-up.
|
||||
for icon in ekklesia-wonderink ekklesia-parent-call; do
|
||||
@@ -310,6 +333,12 @@ install -Dm755 \
|
||||
"$ROOT/scripts/classroom-theme" \
|
||||
"$STAGE/usr/local/bin/classroom-theme"
|
||||
|
||||
# Converges a machine that used to run something else. Called from
|
||||
# postinst on every install, and by hand by a technician.
|
||||
install -Dm755 \
|
||||
"$ROOT/scripts/classroom-migrate" \
|
||||
"$STAGE/usr/local/bin/classroom-migrate"
|
||||
|
||||
install -Dm644 \
|
||||
"$ROOT/lib/classroom_audio.py" \
|
||||
"$STAGE/usr/local/lib/ekklesia-classroom/classroom_audio.py"
|
||||
|
||||
Executable
+205
@@ -0,0 +1,205 @@
|
||||
#!/bin/bash
|
||||
# Is this machine the classroom?
|
||||
#
|
||||
# One list of properties, run against whatever the lab is currently
|
||||
# holding. A fresh install and a machine upgraded from the old sway or
|
||||
# Hyprland shell have to answer it identically — that is the whole point
|
||||
# of the migration work, and the reason this is a file rather than a
|
||||
# paragraph in a report.
|
||||
#
|
||||
# tools/lab/converged-check the properties
|
||||
# tools/lab/converged-check --restart and again after a session restart
|
||||
#
|
||||
# Every check reads the running machine. Nothing here consults the
|
||||
# repository, so a check cannot pass by agreeing with the source it came
|
||||
# from.
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
LAB="$ROOT/tools/lab/lab"
|
||||
NAME="${LAB_NAME:-classroom-v2-lab}"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
in_lab() { docker exec "$NAME" bash -c "$1" 2>/dev/null; }
|
||||
|
||||
# As the session itself reads configuration: the cascade of the running
|
||||
# shell, not one this script made up.
|
||||
session_config() {
|
||||
local dirs
|
||||
dirs=$(in_lab '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 HOME=/home/classroom \
|
||||
-e LANG=C.UTF-8 -e "XDG_CONFIG_DIRS=$dirs" "$NAME" \
|
||||
kreadconfig6 "$@" 2>/dev/null | tr -d '\r'
|
||||
}
|
||||
|
||||
running() { in_lab "pgrep -x $1 | wc -l"; }
|
||||
|
||||
# Which process is actually answering org.freedesktop.Notifications. On a
|
||||
# machine that still has mako installed this is the question that matters:
|
||||
# the name is taken by whoever gets there first.
|
||||
notification_owner() {
|
||||
local pid
|
||||
pid=$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- \
|
||||
busctl --user --no-pager call org.freedesktop.DBus /org/freedesktop/DBus \
|
||||
org.freedesktop.DBus GetConnectionUnixProcessID s org.freedesktop.Notifications' \
|
||||
| awk '{print $2}')
|
||||
[[ -n "$pid" ]] && in_lab "cat /proc/$pid/comm"
|
||||
}
|
||||
|
||||
# logind answers for the running configuration, not for a file on disk.
|
||||
logind_property() {
|
||||
in_lab "busctl get-property org.freedesktop.login1 /org/freedesktop/login1 \
|
||||
org.freedesktop.login1.Manager $1" | cut -d'"' -f2
|
||||
}
|
||||
|
||||
APPLETS=/home/classroom/.config/plasma-org.kde.plasma.desktop-appletsrc
|
||||
|
||||
check() {
|
||||
# check DESCRIPTION EXPECTED ACTUAL
|
||||
if [[ "$2" == "$3" ]]; then
|
||||
printf ' \033[32mPASS\033[0m %-52s %s\n' "$1" "$3"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
printf ' \033[31mFAIL\033[0m %-52s got [%s] want [%s]\n' "$1" "$3" "$2"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
section() { printf '\n%s\n' "$1"; }
|
||||
|
||||
run_checks() {
|
||||
section "the session"
|
||||
check "kwin_wayland running" 1 "$(running kwin_wayland)"
|
||||
check "plasmashell running" 1 "$(running plasmashell)"
|
||||
|
||||
section "the old shell is gone"
|
||||
check "waybar not running" 0 "$(running waybar)"
|
||||
check "mako not running" 0 "$(running mako)"
|
||||
# The bracket keeps the pattern from matching the command that carries it.
|
||||
check "classroom dock not running" 0 "$(in_lab 'pgrep -f "[c]lassroom-dock" | wc -l')"
|
||||
# The lab's own outer compositor is sway, so the question is whether any
|
||||
# sway is running that is not the harness's.
|
||||
check "no sway but the lab harness's" 0 \
|
||||
"$(in_lab 'pgrep -x sway | while read -r p; do tr "\0" " " < /proc/$p/cmdline | grep -q outer-sway.conf || echo x; done | grep -c x || true')"
|
||||
check "Hyprland not running" 0 "$(running Hyprland)"
|
||||
check "waybar activation removed" 0 \
|
||||
"$(in_lab 'find /etc/systemd/user /etc/xdg/systemd/user /home/classroom/.config/systemd/user -name "waybar.service" -path "*.wants/*" 2>/dev/null | wc -l')"
|
||||
check "mako activation removed" 0 \
|
||||
"$(in_lab 'find /etc/systemd/user /etc/xdg/systemd/user /home/classroom/.config/systemd/user -name "mako.service" -path "*.wants/*" 2>/dev/null | wc -l')"
|
||||
check "waybar masked" masked "$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- systemctl --user is-enabled waybar.service || true')"
|
||||
check "mako masked" masked "$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- systemctl --user is-enabled mako.service || true')"
|
||||
check "no login shell starting a compositor" 0 \
|
||||
"$(in_lab 'grep -lE "exec (sway|Hyprland|start-hyprland|classroom-session)" /home/classroom/.bash_profile 2>/dev/null | wc -l')"
|
||||
check "no tty1 autologin drop-in" 0 \
|
||||
"$(in_lab 'ls /etc/systemd/system/[email protected]/autologin.conf 2>/dev/null | wc -l')"
|
||||
check "no autostart entry for the old shell" 0 \
|
||||
"$(in_lab 'grep -lE "^Exec=.*(waybar|mako|classroom-dock)" /home/classroom/.config/autostart/*.desktop 2>/dev/null | wc -l')"
|
||||
|
||||
section "device.conf"
|
||||
check "COMPOSITOR removed" 0 "$(in_lab 'grep -c "^COMPOSITOR=" /etc/ekklesia-classroom/device.conf || true')"
|
||||
check "not on the hyprland channel" 0 "$(in_lab 'grep -c "^CHANNEL=hyprland" /etc/ekklesia-classroom/device.conf || true')"
|
||||
|
||||
section "the configuration cascade"
|
||||
check "classroom settings in the session's cascade" 1 \
|
||||
"$(in_lab 'p=$(pgrep -x plasmashell | head -1); tr "\0" "\n" < /proc/$p/environ | grep -c "^XDG_CONFIG_DIRS=.*ekklesia-classroom/xdg"')"
|
||||
check "look and feel authoritative" org.ekklesia.classroom.desktop \
|
||||
"$(session_config --file kdeglobals --group KDE --key LookAndFeelPackage)"
|
||||
check "and immutable" 1 \
|
||||
"$(in_lab 'grep -c "^LookAndFeelPackage\[\$i\]=" /etc/ekklesia-classroom/xdg/kdeglobals')"
|
||||
check "the palette is the packaged one" 20,28,32 \
|
||||
"$(session_config --file kdeglobals --group 'Colors:Window' --key BackgroundNormal)"
|
||||
|
||||
section "the panel"
|
||||
check "applet count" 10 "$(in_lab "grep -c '^plugin=' $APPLETS")"
|
||||
check "Parent Call launcher" 1 \
|
||||
"$(in_lab "grep -c 'ekklesia-parent-call.desktop' $APPLETS")"
|
||||
check "quick launch, one widget" 1 \
|
||||
"$(in_lab "grep -c '^plugin=org.kde.plasma.quicklaunch' $APPLETS")"
|
||||
check "the applications are the room's" 1 \
|
||||
"$(in_lab "grep -c 'launchers=applications:ekklesia-wonderink.desktop,applications:google-chrome.desktop' $APPLETS")"
|
||||
check "network, volume, notifications, clock" 4 \
|
||||
"$(in_lab "grep -cE '^plugin=org.kde.plasma.(networkmanagement|volume|notifications|digitalclock)$' $APPLETS")"
|
||||
check "no kickoff" 0 "$(in_lab "grep -c 'plugin=org.kde.plasma.kickoff' $APPLETS")"
|
||||
check "no pager" 0 "$(in_lab "grep -c 'plugin=org.kde.plasma.pager' $APPLETS")"
|
||||
check "no system tray" 0 "$(in_lab "grep -c 'systemtray' $APPLETS")"
|
||||
check "no discover or system settings" 0 \
|
||||
"$(in_lab "grep -cE 'discover.desktop|systemsettings.desktop' $APPLETS")"
|
||||
check "the packaged wallpaper" 1 \
|
||||
"$(in_lab "grep -c 'Image=file:///usr/share/backgrounds/ekklesia/classroom-calm.png' $APPLETS")"
|
||||
|
||||
section "it will not strand a teacher"
|
||||
check "no automatic lock" false "$(session_config --file kscreenlockerrc --group Daemon --key Autolock)"
|
||||
check "no lock on resume" false "$(session_config --file kscreenlockerrc --group Daemon --key LockOnResume)"
|
||||
check "the lock action is forbidden" false \
|
||||
"$(session_config --file kdeglobals --group 'KDE Action Restrictions' --key action/lock_screen)"
|
||||
check "power button does nothing" 0 \
|
||||
"$(session_config --file powermanagementprofilesrc --group AC --group HandleButtonEvents --key powerButtonAction)"
|
||||
check "no idle suspend, dim or blank configured" 0 \
|
||||
"$(in_lab 'grep -cE "^\[(AC|Battery|LowBattery)\]\[(SuspendSession|DimDisplay|DPMSControl)\]" /etc/ekklesia-classroom/xdg/powermanagementprofilesrc || true')"
|
||||
check "and the profiles are immutable" 3 \
|
||||
"$(in_lab 'grep -cE "^\[(AC|Battery|LowBattery)\]\[\\\$i\]" /etc/ekklesia-classroom/xdg/powermanagementprofilesrc || true')"
|
||||
check "logind ignores the power key" ignore "$(logind_property HandlePowerKey)"
|
||||
check "logind ignores a long press" ignore "$(logind_property HandlePowerKeyLongPress)"
|
||||
check "logind has no idle action" ignore "$(logind_property IdleAction)"
|
||||
check "logind ignores the lid" ignore "$(logind_property HandleLidSwitch)"
|
||||
|
||||
section "lockdown still in force"
|
||||
for key in run_command shell_access logout; do
|
||||
check "$key" false "$(session_config --file kdeglobals --group 'KDE Action Restrictions' --key "$key")"
|
||||
done
|
||||
check "the panel cannot be edited" false \
|
||||
"$(session_config --file kdeglobals --group 'KDE Action Restrictions' --key plasma/plasmashell/unlockedDesktop)"
|
||||
|
||||
section "autologin"
|
||||
check "sddm enabled" enabled "$(in_lab 'systemctl is-enabled sddm.service || true')"
|
||||
# Not a reading of the configuration: what SDDM actually did at the last
|
||||
# boot of this machine. It authenticates the classroom through PAM and
|
||||
# launches the session; the lab has no display hardware for the session to
|
||||
# land on, so everything past this point is a hardware test.
|
||||
check "sddm autologged the classroom in at boot" yes \
|
||||
"$(in_lab 'journalctl -u sddm --no-pager 2>/dev/null | grep -q "Authentication for user .classroom. successful" && echo yes || echo no')"
|
||||
check "and chose the Plasma session" yes \
|
||||
"$(in_lab 'journalctl -u sddm --no-pager 2>/dev/null | grep -q "wayland-sessions/plasma.desktop. selected" && echo yes || echo no')"
|
||||
check "autologin user" classroom \
|
||||
"$(in_lab 'grep -A3 "^\[Autologin\]" /etc/sddm.conf.d/ekklesia-classroom.conf | sed -n "s/^User=//p"')"
|
||||
check "autologin session" plasma \
|
||||
"$(in_lab 'grep -A3 "^\[Autologin\]" /etc/sddm.conf.d/ekklesia-classroom.conf | sed -n "s/^Session=//p"')"
|
||||
check "the session it names exists" 1 \
|
||||
"$(in_lab 'ls /usr/share/wayland-sessions/plasma.desktop 2>/dev/null | wc -l')"
|
||||
check "the classroom uid is inside the greeter's range" 1 \
|
||||
"$(in_lab 'uid=$(id -u classroom); min=$(sed -n "s/^MinimumUid=//p" /etc/sddm.conf.d/ekklesia-classroom.conf); max=$(sed -n "s/^MaximumUid=//p" /etc/sddm.conf.d/ekklesia-classroom.conf); [ "$uid" -ge "$min" ] && [ "$uid" -le "$max" ] && echo 1 || echo 0')"
|
||||
|
||||
section "notifications belong to Plasma"
|
||||
check "the old daemon cannot be activated" 1 \
|
||||
"$(in_lab 'grep -c "^Exec=/bin/false" /usr/local/share/dbus-1/services/fr.emersion.mako.service 2>/dev/null || true')"
|
||||
check "and Plasma owns the notification service" plasmashell "$(notification_owner)"
|
||||
|
||||
section "the room's own parts survived"
|
||||
check "parent call installed" 1 "$(in_lab 'ls /usr/local/bin/classroom-parent-call 2>/dev/null | wc -l')"
|
||||
check "parent call icon installed" 1 \
|
||||
"$(in_lab 'ls /usr/share/icons/hicolor/scalable/apps/ekklesia-parent-call.svg 2>/dev/null | wc -l')"
|
||||
check "wonderink installed" 1 "$(in_lab 'ls /usr/local/bin/classroom-wonderink 2>/dev/null | wc -l')"
|
||||
check "the maintenance chord is registered" 1 \
|
||||
"$(in_lab 'grep -c "^X-KDE-Shortcuts=Meta+Ctrl+Alt+Shift+K" /usr/share/applications/ekklesia-admin-terminal.desktop || true')"
|
||||
check "no sudo rule for the classroom" 0 \
|
||||
"$(in_lab 'grep -rl classroom /etc/sudoers /etc/sudoers.d 2>/dev/null | wc -l')"
|
||||
check "the panel is supervised" always \
|
||||
"$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- systemctl --user show plasma-plasmashell.service -p Restart --value')"
|
||||
check "audio policy active" active \
|
||||
"$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- systemctl --user is-active ekklesia-classroom-audio.service || true')"
|
||||
}
|
||||
|
||||
run_checks
|
||||
|
||||
if [[ "${1:-}" == "--restart" ]]; then
|
||||
section "and again after logging out and back in"
|
||||
"$LAB" plasma-down >/dev/null
|
||||
"$LAB" plasma-up >/dev/null
|
||||
sleep 3
|
||||
run_checks
|
||||
fi
|
||||
|
||||
printf '\n%s passed, %s failed\n' "$PASS" "$FAIL"
|
||||
[[ "$FAIL" -eq 0 ]]
|
||||
Executable
+236
@@ -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"
|
||||
+10
-3
@@ -27,9 +27,11 @@ docker exec "$NAME" sh -c "
|
||||
state=$(docker exec "$NAME" dpkg-query -W -f='${db:Status-Abbrev}' ekklesia-classroom)
|
||||
echo "package state: $state"
|
||||
|
||||
# environment.d is read when the user manager starts, so it starts after.
|
||||
docker exec "$NAME" systemctl restart [email protected]e
|
||||
sleep 4
|
||||
# And then the machine is restarted, the way a room takes an update: a
|
||||
# new PID 1, a new user manager, SDDM autologging the classroom in. The
|
||||
# session environment, the logind drop-in and the user services are all
|
||||
# things that are only read at boot.
|
||||
"$LAB" reboot
|
||||
|
||||
"$LAB" session >/dev/null
|
||||
sleep 8
|
||||
@@ -50,3 +52,8 @@ check "panel restart policy" "$(docker exec -u classroom -e XDG_RUNTIME_DIR=/run
|
||||
check "audio policy" "$(docker exec -u classroom -e XDG_RUNTIME_DIR=/run/user/1000 \
|
||||
-e DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus "$NAME" \
|
||||
systemctl --user is-active ekklesia-classroom-audio.service || true)"
|
||||
|
||||
# The same list of properties the upgraded machine has to satisfy. A
|
||||
# fresh install and a migrated one are supposed to be indistinguishable
|
||||
# from a teacher's chair, so they are asked the same questions.
|
||||
"$ROOT/tools/lab/converged-check" ${CHECK_ARGS:-}
|
||||
|
||||
@@ -12,16 +12,26 @@ SRC="${1:-/srv/classroom}"
|
||||
# A directory of the classroom's own, ahead of /etc/xdg in
|
||||
# XDG_CONFIG_DIRS. plasma-workspace owns files by these names in /etc/xdg
|
||||
# and two packages cannot own one path.
|
||||
for setting in kdeglobals kwinrc kwinrulesrc kscreenlockerrc ksmserverrc \
|
||||
for setting in kwinrc kwinrulesrc kscreenlockerrc ksmserverrc \
|
||||
krunnerrc plasmanotifyrc powermanagementprofilesrc; do
|
||||
install -Dm644 "$SRC/config/plasma/xdg/$setting" \
|
||||
"/etc/ekklesia-classroom/xdg/$setting"
|
||||
rm -f "/etc/xdg/$setting.ekklesia-old"
|
||||
done
|
||||
|
||||
# Composed, not copied — the policy and the palette. See tools/build-kdeglobals.
|
||||
install -d /etc/ekklesia-classroom/xdg
|
||||
"$SRC/tools/build-kdeglobals" > /etc/ekklesia-classroom/xdg/kdeglobals
|
||||
chmod 0644 /etc/ekklesia-classroom/xdg/kdeglobals
|
||||
|
||||
install -Dm644 "$SRC/config/environment/90-ekklesia-classroom.conf" \
|
||||
/etc/environment.d/90-ekklesia-classroom.conf
|
||||
|
||||
# What actually puts that directory into a session started by a display
|
||||
# manager. startplasma sources this; PAM never reads environment.d.
|
||||
install -Dm644 "$SRC/config/plasma/env/zz-ekklesia-classroom.sh" \
|
||||
/etc/xdg/plasma-workspace/env/zz-ekklesia-classroom.sh
|
||||
|
||||
install -Dm644 "$SRC/config/systemd/logind-ekklesia-classroom.conf" \
|
||||
/etc/systemd/logind.conf.d/ekklesia-classroom.conf
|
||||
|
||||
@@ -53,6 +63,9 @@ done
|
||||
install -Dm644 "$SRC/config/plasma/solid/ekklesia-dvd.desktop" \
|
||||
/usr/share/solid/actions/ekklesia-dvd.desktop
|
||||
|
||||
install -Dm644 "$SRC/config/dbus/fr.emersion.mako.service" \
|
||||
/usr/local/share/dbus-1/services/fr.emersion.mako.service
|
||||
|
||||
for icon in "$SRC"/config/icons/ekklesia-*.svg; do
|
||||
[ -e "$icon" ] || continue
|
||||
install -Dm644 "$icon" \
|
||||
|
||||
+76
-3
@@ -35,6 +35,17 @@ outer() {
|
||||
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 \
|
||||
@@ -74,6 +85,55 @@ down)
|
||||
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
|
||||
|
||||
# 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
|
||||
@@ -103,7 +163,6 @@ plasma-up)
|
||||
-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" \
|
||||
-e XDG_CONFIG_DIRS=/etc/ekklesia-classroom/xdg:/etc/xdg \
|
||||
-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
|
||||
@@ -114,9 +173,22 @@ plasma-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
|
||||
docker exec "$NAME" pkill -u classroom -x plasmashell || true
|
||||
docker exec "$NAME" pkill -u classroom -x kwin_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"
|
||||
;;
|
||||
@@ -200,6 +272,7 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user