The update check has been daily at 03:00, Persistent, since 0.1.5 — the lab's installed stable machine logged it at 03:01 and 03:04 on successive days — and it stays so. What changes is the reboot after an update. It happened the moment the update was installed: harmless at 03:00, and a room restarting itself mid-morning when a computer switched off at 03:00 caught up after being switched on. Now the updater installs, with every check it had (the device's own channel, a newer version, the manifest's SHA-256, name and version), and starts classroom-reboot-when-idle, which reboots once no input device has produced an event for 30 minutes and no sound card has played for as long. At 03:00 that is about 03:30; switched off first, the next start is on the new version. The service has no [Install] and nothing enables it. The release gate checks after every install and upgrade that the timer is enabled, daily at 03:00, Persistent, with no boot or repeat trigger and no drop-in, and that the updater no longer reboots by itself. Co-Authored-By: Claude Opus 5.5 <[email protected]>
543 lines
24 KiB
Bash
Executable File
543 lines
24 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)"
|
|
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" ]]
|
|
[[ "$(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
|
|
|
|
# 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
|
|
|
|
# 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
|