The DVD player was the only thing still speaking a compositor the room no longer runs. It asked sway's tree whether VLC's window had really gone fullscreen, because VLC reports playing while its window is still the black startup one the loading cover exists to hide. VLC's own status was measured as a replacement and is not one: it reports fullscreen true in the same 250ms poll as playing, because the flag is the option it was started with and not a report about its window. So the cover is held for a floor and capped by the deadline that was already there, and the compositor is not asked anything. Focus-or-launch went the same way. The session already takes an exclusive lock, so a second one cannot start, and bringing an existing one to the front is the panel's job. classroom-window, classroom_sway and classroom_chrome are gone with it — the last of them was reachable from nothing at all. DVD also moves out of the status row, where it sat beside the clock as though a disc were a machine reading. It is an application: it is in the row with Wonder Ink and the browser, and classroom-provision removes it from the layout on a machine with no optical drive, which retires DVD_ENABLED. A computer that moves between rooms takes its drive with it; nobody remembers to edit a file when it does. The panel is rebuilt from the package at every login now. Plasma keeps whatever panel it first built, so an update could change the bar in the package and never change it in a room — measured, with an icon that outlived the file it came from. Editing the live panel configuration was tried instead and destroyed the panel, because the shell owns that file. Removing it before the shell starts is safe and makes the desktop stateless. Co-Authored-By: Claude Opus 5 <[email protected]>
154 lines
7.0 KiB
Bash
Executable File
154 lines
7.0 KiB
Bash
Executable File
#!/bin/bash
|
|
set -uo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
python3 - "$ROOT" <<'PY'
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
root = Path(sys.argv[1])
|
|
control = (root / "packaging/debian/control.template").read_text()
|
|
postinst = (root / "packaging/debian/postinst").read_text()
|
|
provision = (root / "scripts/classroom-provision").read_text()
|
|
units = "\n".join(path.read_text() for path in (root / "systemd").rglob("*.service"))
|
|
canary = (root / "update_canary.sh").read_text()
|
|
validation_script = (root / "tools/validate-trixie-release.sh").read_text()
|
|
layout = (root / "config/plasma/lookandfeel/contents/layouts/org.kde.plasma.desktop-layout.js").read_text()
|
|
rules = (root / "config/plasma/xdg/kwinrulesrc").read_text()
|
|
restrictions = (root / "config/plasma/xdg/kdeglobals").read_text()
|
|
supervision = (root / "systemd/user/plasma-plasmashell.service.d/ekklesia-classroom.conf").read_text()
|
|
build = (root / "tools/build-release.sh").read_text()
|
|
policy = json.loads(
|
|
(root / "config/chrome/policies/managed/ekklesia-classroom.json").read_text()
|
|
)
|
|
|
|
passed = failed = 0
|
|
|
|
|
|
def check(label, condition):
|
|
global passed, failed
|
|
if condition:
|
|
print(f" ok {label}")
|
|
passed += 1
|
|
else:
|
|
print(f" FAIL {label}")
|
|
failed += 1
|
|
|
|
|
|
depends = re.search(r"^Depends: (.*)$", control, re.MULTILINE).group(1).split(", ")
|
|
|
|
print("1. Trixie package contract")
|
|
for package in (
|
|
"network-manager",
|
|
"cups",
|
|
"cups-client",
|
|
"cups-filters",
|
|
"foomatic-db-compressed-ppds",
|
|
):
|
|
check(f"depends on {package}", package in depends)
|
|
|
|
print()
|
|
print("2. networking service behavior")
|
|
check("NetworkManager is explicitly enabled", "systemctl enable NetworkManager.service || true" in postinst)
|
|
check("NetworkManager start is best effort", "systemctl start NetworkManager.service || true" in postinst)
|
|
check("online waiter is disabled", "systemctl disable NetworkManager-wait-online.service || true" in postinst)
|
|
check("no classroom NetworkManager config overrides ifupdown", not (root / "config/network").exists())
|
|
|
|
print()
|
|
print("3. printing service behavior")
|
|
check("CUPS service and socket are enabled", "systemctl enable cups.socket cups.service || true" in postinst)
|
|
check("CUPS start is best effort", "systemctl start cups.socket || true" in postinst)
|
|
check("automatic remote queue browsing is disabled", "systemctl disable --now cups-browsed.service || true" in postinst)
|
|
check("Avahi service and socket are disabled", "systemctl disable --now avahi-daemon.socket avahi-daemon.service || true" in postinst)
|
|
check("Avahi service and socket are masked", "systemctl mask avahi-daemon.socket avahi-daemon.service || true" in postinst)
|
|
check("printer setup is not a blocking boot dependency", "classroom-printer-setup" not in units)
|
|
check("printer setup runs automatically in provisioning", "/usr/local/bin/classroom-printer-setup" in provision)
|
|
check("printer provisioning is bounded", "--kill-after=5s 30s" in provision)
|
|
check("Chrome uses the system print dialog", policy.get("DisablePrintPreview") is True)
|
|
|
|
print()
|
|
print("4. secondary windows are the window manager's business")
|
|
# Chrome gives its main window and every window a page opens the same
|
|
# class, so a rule keyed on that class maximises the lesson's popup too.
|
|
# It was measured doing exactly that. The browser is told to start
|
|
# maximized instead, and no rule mentions it.
|
|
check("no window rule claims the browser", "google-chrome" not in rules)
|
|
check("the browser asks for its own first window", "--start-maximized" in
|
|
(root / "scripts/classroom-browser").read_text())
|
|
check("Wonder Ink asks for the same", "--start-maximized" in
|
|
(root / "scripts/classroom-wonderink").read_text())
|
|
check("the Chrome window arbiter is gone", not (root / "scripts/classroom-chrome-windows").exists())
|
|
check("and is not packaged", "classroom-chrome-windows" not in build)
|
|
|
|
print()
|
|
print("4b. the panel is never allowed to stay dead")
|
|
# Plasma restarts a crashed panel three times a minute and then gives up.
|
|
# Measured: dead on the third kill. These lines are what make it
|
|
# unconditional, and losing them would lose the room its only way around.
|
|
check("restart is unconditional", "Restart=always" in supervision)
|
|
check("the burst limit is lifted", "StartLimitIntervalSec=0" in supervision)
|
|
check("the drop-in is packaged", "plasma-plasmashell.service.d" in build)
|
|
|
|
print()
|
|
print("4b2. the session settings do not fight another package")
|
|
# plasma-workspace owns /etc/xdg/kdeglobals and friends. dpkg refused the
|
|
# package when the classroom tried to own them too, so the room's copies
|
|
# live in a directory of their own, in front of /etc/xdg.
|
|
check("settings install to the classroom's own directory",
|
|
"$STAGE/etc/ekklesia-classroom/xdg/" in build)
|
|
check("nothing is installed into /etc/xdg", "$STAGE/etc/xdg/" not in build)
|
|
check("and that directory is searched first",
|
|
"XDG_CONFIG_DIRS=/etc/ekklesia-classroom/xdg:/etc/xdg" in
|
|
(root / "config/environment/90-ekklesia-classroom.conf").read_text())
|
|
check("the environment file is packaged",
|
|
"/etc/environment.d/90-ekklesia-classroom.conf" in build)
|
|
|
|
print()
|
|
print("4b3. every icon the panel names is in the package")
|
|
import configparser
|
|
for entry in sorted((root / "config/plasma/applications").glob("*.desktop")):
|
|
parser = configparser.ConfigParser(interpolation=None, strict=False)
|
|
parser.read(entry)
|
|
icon = parser["Desktop Entry"].get("icon", "")
|
|
if not icon.startswith("ekklesia-"):
|
|
continue
|
|
check(f"{icon} is installed by the package",
|
|
f'"$ROOT/config/icons/${{icon}}.svg"' in build or icon in build)
|
|
check(f"{icon}.svg exists", (root / f"config/icons/{icon}.svg").is_file())
|
|
|
|
print()
|
|
print("4c. the teacher cannot reach a command line")
|
|
for restriction in ("run_command=false", "shell_access=false",
|
|
"action/lock_screen=false",
|
|
"plasma/plasmashell/unlockedDesktop=false"):
|
|
check(f"{restriction}", restriction in restrictions)
|
|
check("the restrictions are immutable", "[KDE Action Restrictions][$i]" in restrictions)
|
|
|
|
print()
|
|
print("4d. clicking a launcher never hides an application")
|
|
check("minimise on click is off", 'minimizeActiveTaskOnClick", false' in layout)
|
|
check("there is no minimise button", "ButtonsOnRight=FX" in
|
|
(root / "config/plasma/xdg/kwinrc").read_text())
|
|
|
|
print()
|
|
print("5. bounded Trixie validation")
|
|
check("container has an outer deadline", "720s" in validation_script)
|
|
check("package installs have deadlines", "candidate-install 360" in validation_script)
|
|
check("container service startup is suppressed", "/usr/sbin/policy-rc.d" in validation_script and "exit 101" in validation_script)
|
|
|
|
print()
|
|
print("6. canary release gate")
|
|
validation = canary.index('"$ROOT/tools/validate-trixie-release.sh"')
|
|
package_copy = canary.index('cp "$package_path"')
|
|
first_rsync = canary.index("rsync -av")
|
|
check("Trixie validation runs before the repository copy", validation < package_copy)
|
|
check("Trixie validation runs before any upload", validation < first_rsync)
|
|
|
|
print()
|
|
print(f"passed: {passed} failed: {failed}")
|
|
raise SystemExit(1 if failed else 0)
|
|
PY
|