The dirty-upgrade lab installed the new package with dpkg -i. Since 0.2.6.2 the package conflicts with khelpcenter, which dpkg refuses and apt resolves, so the 0.2.2 rehearsal left the machine on 0.2.2 and asked every question of the old release. Old package, new package and the reinstall now all go through apt, as ekklesia-classroom-update does, and each stops the run unless dpkg reports exactly that version, fully installed. The container gate asserts the same of its previous release and its reinstall. Co-Authored-By: Claude Opus 5.5 <[email protected]>
618 lines
27 KiB
Bash
Executable File
618 lines
27 KiB
Bash
Executable File
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
if [[ $# -lt 1 || $# -gt 2 ]]; then
|
|
echo "Usage: $0 CANDIDATE.deb [PREVIOUS.deb]" >&2
|
|
exit 2
|
|
fi
|
|
|
|
candidate="$(realpath "$1")"
|
|
previous=""
|
|
|
|
[[ -f "$candidate" ]] || { echo "Package not found: $candidate" >&2; exit 1; }
|
|
|
|
if [[ $# -eq 2 ]]; then
|
|
previous="$(realpath "$2")"
|
|
[[ -f "$previous" ]] || { echo "Package not found: $previous" >&2; exit 1; }
|
|
fi
|
|
|
|
# The channel the upgraded room is on. canary by default, which is the
|
|
# machine a new release goes to first; stable to rehearse the day the
|
|
# desktops are moved, from the release they are actually on.
|
|
channel="${VALIDATION_CHANNEL:-canary}"
|
|
|
|
case "$channel" in
|
|
stable|canary) ;;
|
|
*) echo "VALIDATION_CHANNEL must be stable or canary" >&2; exit 2 ;;
|
|
esac
|
|
|
|
echo "Validating $(basename "$candidate") on Debian Trixie"
|
|
|
|
docker_args=(
|
|
run --rm -i
|
|
-v "$candidate:/packages/candidate.deb:ro"
|
|
)
|
|
|
|
if [[ -n "$previous" ]]; then
|
|
docker_args+=( -v "$previous:/packages/previous.deb:ro" )
|
|
fi
|
|
|
|
# 720s was enough while this package's heaviest dependency was Plasma.
|
|
# 0.2.6 adds LibreOffice and a dozen font packages for Presentations, and
|
|
# the run installs the PREVIOUS package as well as the candidate, so the
|
|
# container now does two full desktop installs. Measured: the previous
|
|
# install alone ran past 360s on the development VM and the whole run was
|
|
# killed before it reached a single assertion.
|
|
#
|
|
# Generous rather than tight, on purpose. This is the gate that has to
|
|
# say "the package is wrong", and a gate that says "the machine was busy"
|
|
# instead teaches everybody to re-run it until it passes.
|
|
timeout --signal=TERM --kill-after=15s 2700s \
|
|
docker "${docker_args[@]}" debian:trixie-slim \
|
|
bash -s -- "${previous:+/packages/previous.deb}" "$channel" <<'CONTAINER'
|
|
set -Eeuo pipefail
|
|
|
|
validation_failure() {
|
|
status=$?
|
|
echo "Validation failed at line ${BASH_LINENO[0]}: ${BASH_COMMAND}" >&2
|
|
|
|
if command -v lpstat >/dev/null 2>&1; then
|
|
lpstat -t >&2 || true
|
|
lpoptions -p Elmer -l >&2 || true
|
|
fi
|
|
|
|
[[ ! -f /tmp/provision-first.log ]] || cat /tmp/provision-first.log >&2
|
|
[[ ! -f /tmp/provision-second.log ]] || cat /tmp/provision-second.log >&2
|
|
exit "$status"
|
|
}
|
|
|
|
trap validation_failure ERR
|
|
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
|
|
# Package validation runs in a container with no init system. Do not let
|
|
# maintainer scripts try to start CUPS, Avahi or other irrelevant daemons;
|
|
# services are inspected and CUPS is started explicitly when the test needs it.
|
|
cat > /usr/sbin/policy-rc.d <<'POLICY'
|
|
#!/bin/sh
|
|
exit 101
|
|
POLICY
|
|
chmod 0755 /usr/sbin/policy-rc.d
|
|
|
|
run_logged() {
|
|
label="$1"
|
|
seconds="$2"
|
|
shift 2
|
|
|
|
if ! timeout --signal=TERM --kill-after=10s "${seconds}s" "$@" \
|
|
>"/tmp/${label}.log" 2>&1; then
|
|
cat "/tmp/${label}.log" >&2
|
|
echo "${label} exceeded ${seconds}s or failed" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
run_logged apt-update 300 apt-get update -qq
|
|
|
|
previous="${1:-}"
|
|
channel="${2:-canary}"
|
|
|
|
if [[ -n "$previous" ]]; then
|
|
run_logged previous-install 1200 apt-get install -y -qq "$previous"
|
|
|
|
cat > /etc/ekklesia-classroom/device.conf <<'CONFIG'
|
|
# Configuration carried forward from 0.1.36
|
|
UPDATE_URL=https://updates.riverofglorymn.org
|
|
CHANNEL=@CHANNEL@
|
|
DVD_ENABLED=false
|
|
UI_SCALE=1.25
|
|
MQTT_ENABLED=true
|
|
MQTT_HOST=broker.example.test
|
|
MQTT_PORT=1884
|
|
MQTT_USERNAME=classroom
|
|
MQTT_PASSWORD=preserve-me
|
|
CUSTOM_SETTING=preserve-me-too
|
|
CONFIG
|
|
sed -i "s/@CHANNEL@/${channel}/" /etc/ekklesia-classroom/device.conf
|
|
previous_version="$(dpkg-query -W -f='${Version}' ekklesia-classroom)"
|
|
|
|
# The release this is an upgrade FROM, proven rather than assumed: an
|
|
# install that silently did not happen makes every check below a
|
|
# check of a fresh install instead.
|
|
[[ "$previous_version" == "$(dpkg-deb --field "$previous" Version)" ]]
|
|
[[ "$(dpkg-query -W -f='${db:Status-Abbrev}' ekklesia-classroom)" == "ii " ]]
|
|
echo "installed the previous release, ekklesia-classroom $previous_version"
|
|
fi
|
|
|
|
run_logged candidate-install 1200 apt-get install -y -qq /packages/candidate.deb
|
|
|
|
# THE VERSION, AS THE MACHINE REPORTS IT.
|
|
#
|
|
# Three places say which release a classroom is running: dpkg's database,
|
|
# which the updater compares against the manifest; the VERSION file the
|
|
# package carries; and the file name the release was published under.
|
|
# They have to be one number, or a room either never updates or updates
|
|
# for ever.
|
|
candidate_version="$(dpkg-deb --field /packages/candidate.deb Version)"
|
|
[[ "$(dpkg-query -W -f='${Version}' ekklesia-classroom)" == "$candidate_version" ]]
|
|
[[ "$(dpkg-query -W -f='${db:Status-Abbrev}' ekklesia-classroom)" == "ii " ]]
|
|
[[ "$(cat /usr/local/lib/ekklesia-classroom/VERSION)" == "$candidate_version" ]]
|
|
echo "installed ekklesia-classroom $candidate_version${previous_version:+ over $previous_version}"
|
|
|
|
if [[ -n "${previous_version:-}" ]]; then
|
|
dpkg --compare-versions "$candidate_version" gt "$previous_version"
|
|
fi
|
|
|
|
command -v nmtui >/dev/null
|
|
command -v lpadmin >/dev/null
|
|
command -v lpinfo >/dev/null
|
|
|
|
systemctl is-enabled NetworkManager.service >/dev/null
|
|
systemctl is-enabled cups.service >/dev/null
|
|
! systemctl is-enabled NetworkManager-wait-online.service >/dev/null 2>&1
|
|
|
|
# The update check: daily at 03:00, caught up after a night switched off,
|
|
# and still so after this install or upgrade. The reboot after an update
|
|
# waits until nobody is using the computer, in a unit nothing enables.
|
|
systemctl is-enabled ekklesia-classroom-update.timer >/dev/null
|
|
timer=/etc/systemd/system/ekklesia-classroom-update.timer
|
|
grep -qx 'OnCalendar=\*-\*-\* 03:00:00' "$timer"
|
|
grep -qx 'Persistent=true' "$timer"
|
|
! grep -qE '^(OnBootSec|OnStartupSec|OnUnitActiveSec|OnUnitInactiveSec|OnActiveSec)=' "$timer"
|
|
! ls /etc/systemd/system/ekklesia-classroom-update.timer.d/*.conf >/dev/null 2>&1
|
|
test -x /usr/local/bin/classroom-reboot-when-idle
|
|
[[ "$(systemctl is-enabled ekklesia-classroom-reboot-when-idle.service 2>/dev/null || true)" == "static" ]]
|
|
! grep -q '"systemctl", "reboot"' /usr/local/bin/ekklesia-classroom-update
|
|
grep -q 'ekklesia-classroom-reboot-when-idle.service' /usr/local/bin/ekklesia-classroom-update
|
|
|
|
# The audio policy reaches a session, and is ordered after the server it
|
|
# actually talks to. Ordering it only after pipewire and wireplumber is
|
|
# what let it race PipeWire's PulseAudio server at boot, fail its one
|
|
# startup attempt and then sit idle — the rooms were silent on 0.2.2 for
|
|
# exactly this reason, so the ordering is asserted rather than assumed.
|
|
audio_unit=/etc/systemd/user/ekklesia-classroom-audio.service
|
|
[[ -f "$audio_unit" ]]
|
|
grep -q 'pipewire-pulse' "$audio_unit"
|
|
[[ -L /etc/systemd/user/plasma-workspace.target.wants/ekklesia-classroom-audio.service ]]
|
|
command -v classroom-audio >/dev/null
|
|
command -v pactl >/dev/null
|
|
|
|
# The classroom account cannot be shown a saved Wi-Fi password. The rule
|
|
# has to be installed, owned by root — polkit silently ignores a rules
|
|
# file that is not — and it has to refuse the action Debian ships as
|
|
# "yes", which is the one a teacher would actually reach.
|
|
network_rules=/usr/share/polkit-1/rules.d/49-ekklesia-classroom-network.rules
|
|
[[ -f "$network_rules" ]]
|
|
[[ "$(stat -c %U:%G "$network_rules")" == "root:root" ]]
|
|
grep -q "settings.modify.own" "$network_rules"
|
|
grep -q "wifi.share.protected" "$network_rules"
|
|
# And it must NOT refuse the actions that keep the room on the network.
|
|
! grep -qE '"org\.freedesktop\.NetworkManager\.network-control"' "$network_rules"
|
|
grep -q "kcm_networkmanagement=false" /etc/ekklesia-classroom/xdg/kdeglobals
|
|
command -v classroom-wifi >/dev/null
|
|
|
|
# Adopting an installer-configured Wi-Fi is an administrator's command,
|
|
# never something an upgrade does on its own: it moves a machine's only
|
|
# link between managers, and the machine it would strand is a laptop in a
|
|
# locked room. The migration may only LOOK.
|
|
classroom-wifi --check 2>&1 | grep -q "no wireless interface"
|
|
classroom-wifi --adopt 2>&1 | grep -q "nothing to adopt"
|
|
! grep -qE '^[^#]*classroom-wifi --adopt' /usr/local/bin/classroom-migrate
|
|
grep -q "classroom-wifi --check" /usr/local/bin/classroom-migrate
|
|
PYTHONPATH=/usr/local/lib/ekklesia-classroom python3 -c \
|
|
'import classroom_network; classroom_network.parse("")'
|
|
|
|
# THE 0.2.5 WI-FI REGRESSION, ASSERTED ON A REAL INSTALL.
|
|
#
|
|
# The prayer-room laptop's network is hidden and its name has a space in
|
|
# it. The migration dropped the hidden flag, so the connection it built
|
|
# could never associate. Checked against the module the package actually
|
|
# installed, not the one in a checkout.
|
|
PYTHONPATH=/usr/local/lib/ekklesia-classroom python3 <<'WIFI_CHECK'
|
|
import classroom_network as n
|
|
|
|
stanza = n.wireless(n.parse(
|
|
'allow-hotplug wlp2s0\n'
|
|
'iface wlp2s0 inet dhcp\n'
|
|
'\twpa-ssid "ROGC Classroom"\n'
|
|
'\twpa-psk se#cret\n'
|
|
'\twpa-scan-ssid 1\n'
|
|
), lambda device: True)[0]
|
|
|
|
found = n.profile(stanza)
|
|
assert found["ssid"] == "ROGC Classroom", found
|
|
assert found["hidden"] is True, "the hidden flag was dropped again"
|
|
assert found["psk"] == "se#cret", "a # in the password was truncated again"
|
|
assert found["key_mgmt"] == "wpa-psk", found
|
|
|
|
# A rollback puts back exactly what it commented out, and nothing else.
|
|
before = ("auto enp1s0\niface enp1s0 inet static\n\taddress 10.40.0.9/24\n\n"
|
|
"allow-hotplug wlp2s0\niface wlp2s0 inet dhcp\n\twpa-ssid Sanctuary\n")
|
|
after = n.comment_out(before, n.wireless(n.parse(before),
|
|
lambda d: d == "wlp2s0")[0])
|
|
restored, count = n.uncomment(after)
|
|
assert count, "nothing was marked, so a rollback would have nothing to undo"
|
|
assert restored == before, "the rollback did not restore the file exactly"
|
|
WIFI_CHECK
|
|
|
|
# And the command itself carries both halves of the fix.
|
|
grep -q '802-11-wireless.hidden' /usr/local/bin/classroom-wifi
|
|
grep -q 'managed", "yes"' /usr/local/bin/classroom-wifi
|
|
|
|
# PRESENTATIONS.
|
|
#
|
|
# Off by default, with everything it needs present on every machine, so
|
|
# enabling it on a device is one line in device.conf and a reboot.
|
|
command -v classroom-presentations >/dev/null
|
|
command -v classroom-presentation-import >/dev/null
|
|
command -v soffice >/dev/null
|
|
command -v pdftoppm >/dev/null
|
|
command -v pdfinfo >/dev/null
|
|
[[ -f /etc/systemd/user/ekklesia-classroom-presentations.service ]]
|
|
# Enabled the same way the audio policy is, so it actually reaches a
|
|
# session rather than merely being installed. A unit file nobody starts
|
|
# is a teacher whose download never appears.
|
|
[[ -L /etc/systemd/user/plasma-workspace.target.wants/ekklesia-classroom-presentations.service ]]
|
|
[[ -f /usr/share/applications/ekklesia-presentations.desktop ]]
|
|
[[ -f /usr/share/icons/hicolor/scalable/apps/ekklesia-presentations.svg ]]
|
|
|
|
# A classroom that has never heard of the setting comes up without it.
|
|
PYTHONPATH=/usr/local/lib/ekklesia-classroom python3 -c \
|
|
'from classroom_config import feature; assert feature({}, "PRESENTATIONS_ENABLED") is False'
|
|
|
|
# Disabled is the default, so both commands decline to run and say why.
|
|
classroom-presentations 2>&1 | grep -q 'PRESENTATIONS_ENABLED is false'
|
|
classroom-presentation-import --once 2>&1 | grep -q 'PRESENTATIONS_ENABLED is false'
|
|
|
|
# THE CONVERSION THIS WHOLE FEATURE RESTS ON.
|
|
#
|
|
# On a real Debian, with the fonts this package installs. No window, no
|
|
# splash, no first-run dialog, no editor — measured rather than assumed,
|
|
# because "LibreOffice is installed" is not the same claim as "a PPTX
|
|
# becomes slides a teacher can be shown".
|
|
mkdir -p /tmp/deck
|
|
python3 <<'MAKE_DECK'
|
|
page = ('<draw:page draw:name="p%d"><draw:frame svg:width="20cm" '
|
|
'svg:height="3cm" svg:x="1cm" svg:y="2cm"><draw:text-box>'
|
|
'<text:p>%s</text:p></draw:text-box></draw:frame></draw:page>')
|
|
|
|
open("/tmp/deck/deck.fodp", "w").write(
|
|
'<?xml version="1.0" encoding="UTF-8"?>'
|
|
'<office:document '
|
|
'xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" '
|
|
'xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" '
|
|
'xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" '
|
|
'xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" '
|
|
'xmlns:presentation='
|
|
'"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0" '
|
|
'office:version="1.3" '
|
|
'office:mimetype="application/vnd.oasis.opendocument.presentation">'
|
|
'<office:body><office:presentation>'
|
|
+ (page % (1, "Wednesday Bible Study"))
|
|
+ (page % (2, "Session 7"))
|
|
+ '</office:presentation></office:body></office:document>')
|
|
MAKE_DECK
|
|
|
|
HOME=/tmp/deck timeout 240 soffice --headless --norestore --nologo \
|
|
--convert-to pptx --outdir /tmp/deck /tmp/deck/deck.fodp >/dev/null 2>&1
|
|
[[ -f /tmp/deck/deck.pptx ]]
|
|
|
|
PYTHONPATH=/usr/local/lib/ekklesia-classroom HOME=/tmp/deck python3 <<'CONVERT_CHECK'
|
|
import pathlib
|
|
|
|
import classroom_presentations as library
|
|
|
|
ok, error = library.convert_to_pdf("/tmp/deck/deck.pptx", "/tmp/deck/slides.pdf")
|
|
assert ok, f"PPTX could not be converted: {error}"
|
|
|
|
pages = library.page_count("/tmp/deck/slides.pdf")
|
|
assert pages == 2, f"expected 2 slides, got {pages}"
|
|
|
|
made, error = library.render_pages("/tmp/deck/slides.pdf", "/tmp/deck/pages")
|
|
assert made == 2, f"expected 2 rendered pages, got {made}: {error}"
|
|
|
|
# The library asks for page-001.png whatever the page count's width is.
|
|
for number in (1, 2):
|
|
path = pathlib.Path(f"/tmp/deck/pages/page-{number:03d}.png")
|
|
assert path.exists(), f"{path} was not rendered"
|
|
assert path.stat().st_size > 0, f"{path} is empty"
|
|
|
|
# Slide 2 is the one a teacher picks as the card picture, so its text has
|
|
# to have survived the conversion.
|
|
text = library.first_text("/tmp/deck/slides.pdf", pages=2)
|
|
assert "Session 7" in text, f"text did not survive the conversion: {text!r}"
|
|
CONVERT_CHECK
|
|
|
|
# The fonts a Google Slides export asks for, as files on disk.
|
|
#
|
|
# Asserted by path rather than through fc-list: fc-list belongs to the
|
|
# fontconfig package, which nothing here pulls in, so the check passed
|
|
# vacuously until it didn't and then failed for a reason that had nothing
|
|
# to do with the fonts. What LibreOffice actually needs is the file.
|
|
[[ -f /usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf ]]
|
|
[[ -f /usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf ]]
|
|
[[ -f /usr/share/fonts/truetype/liberation/LiberationSansNarrow-Regular.ttf ]]
|
|
[[ -f /usr/share/fonts/truetype/crosextra/Carlito-Regular.ttf ]]
|
|
[[ -f /usr/share/fonts/truetype/crosextra/Caladea-Regular.ttf ]]
|
|
[[ -f /usr/share/fonts/truetype/croscore/Arimo-Regular.ttf ]]
|
|
# And the one that must NOT be here, because it prompts for a licence.
|
|
! dpkg -l ttf-mscorefonts-installer 2>/dev/null | grep -q '^ii'
|
|
|
|
# THE MANUAL.
|
|
#
|
|
# Installed on every machine, works with no network at all, and shows
|
|
# only the features this device has.
|
|
[[ -f /usr/local/share/ekklesia-classroom/manual/classroom-manual.html ]]
|
|
[[ -f /usr/local/share/ekklesia-classroom/manual/classroom-manual.pdf ]]
|
|
command -v classroom-manual >/dev/null
|
|
[[ -f /usr/share/applications/ekklesia-manual.desktop ]]
|
|
head -c 5 /usr/local/share/ekklesia-classroom/manual/classroom-manual.pdf \
|
|
| grep -q '%PDF-'
|
|
grep -aq 'Revision' /usr/local/share/ekklesia-classroom/manual/classroom-manual.pdf
|
|
! grep -qE 'https?://' \
|
|
/usr/local/share/ekklesia-classroom/manual/classroom-manual.html
|
|
|
|
# The manifest the manual reads carries feature NAMES and nothing else:
|
|
# no broker password, no addresses, nothing else out of device.conf.
|
|
printf 'PRESENTATIONS_ENABLED=true\nWONDERINK_ENABLED=false\nMQTT_PASSWORD=never-a-teacher\n' \
|
|
>> /etc/ekklesia-classroom/device.conf
|
|
classroom-desktop-layout >/dev/null 2>&1 || true
|
|
[[ -f /run/ekklesia-classroom/features.json ]]
|
|
grep -q 'PRESENTATIONS_ENABLED' /run/ekklesia-classroom/features.json
|
|
! grep -q 'WONDERINK_ENABLED' /run/ekklesia-classroom/features.json
|
|
! grep -q 'never-a-teacher' /run/ekklesia-classroom/features.json
|
|
! grep -q '10.40.0.2' /run/ekklesia-classroom/features.json
|
|
classroom-manual --features | grep -qE 'PRESENTATIONS_ENABLED +shown'
|
|
classroom-manual --features | grep -qE 'WONDERINK_ENABLED +hidden'
|
|
|
|
# The panel offers Presentations only where it is enabled, keeps the classroom
|
|
# applications in order, and does not expose the separately installed manual
|
|
# as a teacher-facing Help launcher.
|
|
layout=/usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/layouts/org.kde.plasma.desktop-layout.js
|
|
grep -q 'var PRESENTATIONS = true;' "$layout"
|
|
grep -q 'ekklesia-presentations.desktop' "$layout"
|
|
! grep -q 'ekklesia-manual.desktop' "$layout"
|
|
python3 - "$layout" <<'ORDER_CHECK'
|
|
import re
|
|
import sys
|
|
|
|
text = open(sys.argv[1]).read()
|
|
order = re.findall(r'launchers\.push\("applications:([^"]+)"\)', text)
|
|
expected = ["ekklesia-wonderink.desktop", "google-chrome.desktop",
|
|
"ekklesia-dvd.desktop", "ekklesia-presentations.desktop"]
|
|
assert order == expected, f"the dock order changed: {order}"
|
|
ORDER_CHECK
|
|
|
|
sed -i '/^PRESENTATIONS_ENABLED=true$/d;/^WONDERINK_ENABLED=false$/d;/^MQTT_PASSWORD=never-a-teacher$/d' \
|
|
/etc/ekklesia-classroom/device.conf
|
|
|
|
# The policy module and the daemon agree on what they import, on a machine
|
|
# rather than in a checkout.
|
|
PYTHONPATH=/usr/local/lib/ekklesia-classroom python3 -c \
|
|
'import classroom_audio; classroom_audio.settled([], "auto", None)'
|
|
|
|
grep -q '^managed=false$' /etc/NetworkManager/NetworkManager.conf
|
|
|
|
# A normal Docker container has Ethernet and no Wi-Fi adapter. Reaching this
|
|
# point proves package configuration did not require Wi-Fi hardware.
|
|
! find /sys/class/net -mindepth 2 -maxdepth 2 -type d -name wireless | grep -q .
|
|
|
|
/usr/sbin/cupsd
|
|
sleep 1
|
|
|
|
driver='foomatic-db-compressed-ppds:0/ppd/foomatic-ppd/Generic-PCL_6_PCL_XL_Printer-pxlcolor.ppd'
|
|
lpinfo -m | grep -q "^${driver} "
|
|
|
|
# Simulate the queue an older release may have left behind, then run the real
|
|
# boot provisioning path. Chrome is stubbed as already installed and DVD is
|
|
# disabled so the test remains entirely local and deterministic.
|
|
lpadmin -p Classroom_Color -v socket://192.0.2.10:9100 -m "$driver"
|
|
install -m755 /bin/true /usr/local/bin/google-chrome
|
|
|
|
timeout --signal=TERM --kill-after=5s 45s classroom-provision \
|
|
>/tmp/provision-first.log
|
|
|
|
! lpstat -p Classroom_Color >/dev/null 2>&1
|
|
lpstat -p Elmer -d -v | grep -q 'socket://10.10.0.155:9100'
|
|
lpstat -l -p Elmer | grep -q 'Description: Elmer'
|
|
lpoptions -p Elmer -l | grep -Eq '^PrintoutMode/.*:.*\*Normal([[:space:]]|$)'
|
|
lpoptions -p Elmer -l | grep -Eq '^ColorModel/.*:.*\*Color([[:space:]]|$)'
|
|
[[ "$(lpstat -e)" == "Elmer" ]]
|
|
[[ "$(systemctl is-enabled avahi-daemon.service 2>/dev/null || true)" == "masked" ]]
|
|
[[ "$(systemctl is-enabled avahi-daemon.socket 2>/dev/null || true)" == "masked" ]]
|
|
|
|
timeout --signal=TERM --kill-after=5s 45s classroom-provision \
|
|
>/tmp/provision-second.log
|
|
grep -q 'Elmer is already correctly configured' /tmp/provision-second.log
|
|
|
|
# THE UPDATER THIS PACKAGE INSTALLED, AGAINST A MANIFEST.
|
|
#
|
|
# Served from this container, so nothing reaches the real update server.
|
|
# The installed version has to be "up to date" against a manifest naming
|
|
# itself — or the room reinstalls and reboots at every check — and has to
|
|
# see the next feature release as newer.
|
|
mkdir -p /tmp/updates/packages "/tmp/updates/$channel"
|
|
cp /packages/candidate.deb "/tmp/updates/packages/ekklesia-classroom_${candidate_version}_amd64.deb"
|
|
|
|
serve_manifest() {
|
|
python3 - "$1" "$channel" <<'MANIFEST'
|
|
import hashlib, json, sys
|
|
version, channel = sys.argv[1], sys.argv[2]
|
|
name = f"ekklesia-classroom_{version}_amd64.deb"
|
|
try:
|
|
digest = hashlib.sha256(open(f"/tmp/updates/packages/{name}", "rb").read()).hexdigest()
|
|
except FileNotFoundError:
|
|
digest = "0" * 64
|
|
json.dump({"version": version, "package": f"/packages/{name}",
|
|
"sha256": digest, "requires_reboot": True},
|
|
open(f"/tmp/updates/{channel}/manifest.json", "w"))
|
|
MANIFEST
|
|
}
|
|
|
|
python3 -m http.server --bind 127.0.0.1 --directory /tmp/updates 8765 \
|
|
>/tmp/updates.log 2>&1 &
|
|
updates_server=$!
|
|
sleep 1
|
|
|
|
cp /etc/ekklesia-classroom/device.conf /tmp/device.conf.before-updater
|
|
sed -i 's|^UPDATE_URL=.*|UPDATE_URL=http://127.0.0.1:8765|' \
|
|
/etc/ekklesia-classroom/device.conf
|
|
|
|
serve_manifest "$candidate_version"
|
|
ekklesia-classroom-update --check >/tmp/updater-same.log 2>&1
|
|
grep -q "Already up to date" /tmp/updater-same.log
|
|
|
|
# The next milestone after whatever this is, and a release every
|
|
# classroom has long since passed.
|
|
IFS=. read -r major minor _ <<< "$candidate_version"
|
|
next_version="${major}.$((minor + 1)).0"
|
|
|
|
serve_manifest "$next_version"
|
|
ekklesia-classroom-update --check >/tmp/updater-next.log 2>&1
|
|
grep -q "Update available: $candidate_version -> $next_version" /tmp/updater-next.log
|
|
|
|
serve_manifest 0.2.2
|
|
ekklesia-classroom-update --check >/tmp/updater-older.log 2>&1
|
|
grep -q "Already up to date" /tmp/updater-older.log
|
|
|
|
echo '{"version": "0.2.06", "package": "/packages/x.deb", "sha256": "x"}' \
|
|
> "/tmp/updates/$channel/manifest.json"
|
|
! ekklesia-classroom-update --check >/tmp/updater-bad.log 2>&1
|
|
grep -q "invalid version" /tmp/updater-bad.log
|
|
|
|
kill "$updates_server"
|
|
cp /tmp/device.conf.before-updater /etc/ekklesia-classroom/device.conf
|
|
|
|
if [[ -n "$previous" ]]; then
|
|
# The room's own values, untouched by the upgrade.
|
|
grep -q '^DVD_ENABLED=false$' /etc/ekklesia-classroom/device.conf
|
|
grep -q '^UI_SCALE=1.25$' /etc/ekklesia-classroom/device.conf
|
|
grep -q '^MQTT_PASSWORD=preserve-me$' /etc/ekklesia-classroom/device.conf
|
|
grep -q '^CUSTOM_SETTING=preserve-me-too$' /etc/ekklesia-classroom/device.conf
|
|
|
|
# The channel a room was put on deliberately survives the upgrade.
|
|
# canary is where the next development release goes, and the Plasma
|
|
# classroom is the stable line now, so nothing here has to move.
|
|
grep -q "^CHANNEL=${channel}\$" /etc/ekklesia-classroom/device.conf
|
|
|
|
# An optional feature an existing room never had stays off. A
|
|
# Presentations launcher appearing on the desktops' panels by itself
|
|
# would be an upgrade changing a classroom, which upgrades do not do.
|
|
[[ "$(grep -c '^PRESENTATIONS_ENABLED=' /etc/ekklesia-classroom/device.conf)" -eq 1 ]]
|
|
grep -q '^PRESENTATIONS_ENABLED=false$' /etc/ekklesia-classroom/device.conf
|
|
classroom-presentations 2>&1 | grep -q 'PRESENTATIONS_ENABLED is false'
|
|
classroom-desktop-layout >/dev/null 2>&1 || true
|
|
grep -q 'var PRESENTATIONS = false;' \
|
|
/usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/layouts/org.kde.plasma.desktop-layout.js
|
|
|
|
# TINY PEBBLES, 0.2.2 -> 0.2.6.4: THE DESKTOP WITH NO WALLPAPER.
|
|
#
|
|
# The room's desktop was containment 10, made by Plasma after the
|
|
# layout had run, with nothing configured on it; it showed Plasma's
|
|
# own wallpaper. What a desktop gets when nothing wrote it is the look
|
|
# and feel's default, and that has to be a package Plasma can find.
|
|
lnf=/usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/defaults
|
|
[[ "$(sed -n '/^\[Wallpaper\]/,/^\[/s/^Image=//p' "$lnf")" == EkklesiaClassroom ]]
|
|
grep -q '"Id": "EkklesiaClassroom"' /usr/share/wallpapers/EkklesiaClassroom/metadata.json
|
|
[[ "$(readlink -f /usr/share/wallpapers/EkklesiaClassroom/contents/images/*.png)" \
|
|
== /usr/share/backgrounds/ekklesia/classroom-calm.png ]]
|
|
grep -qx 'ExecStartPre=-/usr/local/bin/classroom-desktop-wallpaper' \
|
|
/etc/systemd/user/plasma-plasmashell.service.d/ekklesia-classroom.conf
|
|
|
|
# And the repair the package installed, on that room's own file: the
|
|
# panel built with no screen took 1 and its applets 2 to 9.
|
|
wallpaper_home=$(mktemp -d)
|
|
cat > "$wallpaper_home/plasma-org.kde.plasma.desktop-appletsrc" <<'APPLETS'
|
|
[Containments][1]
|
|
formfactor=2
|
|
location=4
|
|
plugin=org.kde.panel
|
|
wallpaperplugin=org.kde.image
|
|
|
|
[Containments][1][Applets][2]
|
|
plugin=org.kde.plasma.quicklaunch
|
|
|
|
[Containments][1][Applets][9]
|
|
plugin=org.kde.plasma.digitalclock
|
|
|
|
[Containments][1][General]
|
|
AppletOrder=2;3;4;5;6;7;8;9
|
|
|
|
[Containments][10]
|
|
activityId=62d3d55d-bd31-4fc8-9a7b-a5b9b2b99b60
|
|
formfactor=0
|
|
immutability=1
|
|
lastScreen=0
|
|
location=0
|
|
plugin=org.kde.plasma.folder
|
|
wallpaperplugin=org.kde.image
|
|
|
|
[Containments][10][General]
|
|
positions={"1920x1080":[]}
|
|
APPLETS
|
|
applets="$wallpaper_home/plasma-org.kde.plasma.desktop-appletsrc"
|
|
panel_before=$(sed -n '/^\[Containments\]\[1\]/,/^$/p' "$applets")
|
|
|
|
! XDG_CONFIG_HOME="$wallpaper_home" classroom-desktop-wallpaper --check >/dev/null
|
|
XDG_CONFIG_HOME="$wallpaper_home" classroom-desktop-wallpaper \
|
|
| grep -q 'configured on desktop 10'
|
|
sed -n '/^\[Containments\]\[10\]\[Wallpaper\]\[org.kde.image\]\[General\]$/,/^$/p' "$applets" \
|
|
| grep -qx 'Image=file:///usr/share/backgrounds/ekklesia/classroom-calm.png'
|
|
[[ "$(sed -n '/^\[Containments\]\[1\]/,/^$/p' "$applets")" == "$panel_before" ]]
|
|
[[ "$(grep -c '^\[Containments\]\[[0-9]*\]$' "$applets")" -eq 2 ]]
|
|
|
|
# Twice changes nothing, and a room that was already right is not
|
|
# written at all.
|
|
repaired=$(sha256sum < "$applets")
|
|
XDG_CONFIG_HOME="$wallpaper_home" classroom-desktop-wallpaper \
|
|
| grep -q 'the classroom wallpaper already'
|
|
[[ "$(sha256sum < "$applets")" == "$repaired" ]]
|
|
XDG_CONFIG_HOME="$wallpaper_home" classroom-desktop-wallpaper --check >/dev/null
|
|
rm -rf "$wallpaper_home"
|
|
|
|
# And a room that was carried through the experiment on its own
|
|
# channel comes home, because that channel is the obsolete one now.
|
|
sed -i 's/^CHANNEL=.*/CHANNEL=classroom-v2/' /etc/ekklesia-classroom/device.conf
|
|
classroom-migrate >/tmp/migrate-from-v2.log 2>&1
|
|
grep -q '^CHANNEL=stable$' /etc/ekklesia-classroom/device.conf
|
|
grep -q '^MQTT_PASSWORD=preserve-me$' /etc/ekklesia-classroom/device.conf
|
|
grep -q '^CUSTOM_SETTING=preserve-me-too$' /etc/ekklesia-classroom/device.conf
|
|
sed -i "s/^CHANNEL=.*/CHANNEL=${channel}/" /etc/ekklesia-classroom/device.conf
|
|
|
|
# Every setting this release has that the room predates, appended
|
|
# exactly once. classroom-migrate hands device.conf to the shared
|
|
# helper after its own work for this, so that a machine converges
|
|
# whether it got here through this postinst or through somebody
|
|
# running the command by hand.
|
|
for setting in LIVE_ENABLED AUDIO_OUTPUT UI_SCALE; do
|
|
[[ "$(grep -c "^${setting}=" /etc/ekklesia-classroom/device.conf)" -eq 1 ]]
|
|
done
|
|
|
|
run_logged candidate-reinstall 900 \
|
|
apt-get install --reinstall -y -qq /packages/candidate.deb
|
|
[[ "$(dpkg-query -W -f='${Version} ${db:Status-Abbrev}' ekklesia-classroom)" \
|
|
== "$candidate_version ii " ]]
|
|
|
|
# An install that changes nothing must change nothing.
|
|
for setting in LIVE_ENABLED AUDIO_OUTPUT UI_SCALE; do
|
|
[[ "$(grep -c "^${setting}=" /etc/ekklesia-classroom/device.conf)" -eq 1 ]]
|
|
done
|
|
grep -q '^MQTT_PASSWORD=preserve-me$' /etc/ekklesia-classroom/device.conf
|
|
grep -q '^CUSTOM_SETTING=preserve-me-too$' /etc/ekklesia-classroom/device.conf
|
|
grep -q "^CHANNEL=${channel}\$" /etc/ekklesia-classroom/device.conf
|
|
|
|
# And by hand, on a machine that is already converged: the command a
|
|
# technician runs must be safe to run twice.
|
|
classroom-migrate >/tmp/migrate-by-hand.log 2>&1
|
|
for setting in LIVE_ENABLED AUDIO_OUTPUT UI_SCALE; do
|
|
[[ "$(grep -c "^${setting}=" /etc/ekklesia-classroom/device.conf)" -eq 1 ]]
|
|
done
|
|
grep -q '^MQTT_PASSWORD=preserve-me$' /etc/ekklesia-classroom/device.conf
|
|
! grep -q 'preserve-me' /tmp/migrate-by-hand.log
|
|
fi
|
|
|
|
echo "Trixie package, upgrade, bounded install, networking, Elmer printing and reinstall checks passed."
|
|
CONTAINER
|