#!/bin/bash # When a classroom checks for updates, measured on the lab's own systemd. # # switched on at the time the timer fires once at its time, and not # again that day # switched off at the time the lab machine is really stopped across the # time, then started: the missed check runs # once after it boots, and does not loop # # A pair of test units stands in for ekklesia-classroom-update: the same # [Timer] settings with the time moved to a minute or two from now (the # real one would update the lab for real). The real timer's own settings, # after installing the package, are the release gate's to check. # # And classroom-reboot-when-idle against real kernel input: a mouse made # with /dev/uinput, moved and then left alone, with a harmless command in # place of the reboot. # # tools/lab/update-schedule-check set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" NAME="${LAB_NAME:-classroom-v2-lab}" LAB="$ROOT/tools/lab/lab" IDLE="${IDLE_COMMAND:-/usr/local/bin/classroom-reboot-when-idle}" LOG=/var/tmp/ekk-schedule-test.log lab() { docker exec -i "$NAME" bash -c "$1"; } passed=0 failed=0 judge() { if eval "$2"; then printf ' \033[32mPASS\033[0m %s\n' "$1" passed=$((passed + 1)) else printf ' \033[31mFAIL\033[0m %s\n' "$1" failed=$((failed + 1)) fi } # A timer like the real one, due at HH:MM:SS local time, with DELAY spread. install_test_timer() { # WHEN DELAY lab "cat > /etc/systemd/system/ekk-schedule-test.service <<'UNIT' [Unit] Description=Stand-in for the update check [Service] Type=oneshot ExecStart=/bin/sh -c 'echo \"fired \$(date +%%s) boot \$(cut -d. -f1 /proc/uptime)\" >> $LOG' UNIT cat > /etc/systemd/system/ekk-schedule-test.timer </dev/null 2>&1" } fired() { lab "grep -c fired $LOG 2>/dev/null || echo 0" | tail -1; } local_in() { lab "date -d '+$1 seconds' +%H:%M:%S"; } # local time SECONDS from now echo "switched on at the time" due=$(local_in 60) install_test_timer "$due" 0 judge "the stand-in is waiting for $due, local time" \ "lab 'systemctl list-timers ekk-schedule-test.timer --no-legend | grep -q ekk-schedule-test'" sleep 80 judge "at $due it checked, once" '[[ $(fired) -eq 1 ]]' sleep 60 judge "and not again (a minute later, still once)" '[[ $(fired) -eq 1 ]]' next=$(lab "systemctl show ekk-schedule-test.timer -p NextElapseUSecRealtime --value") tomorrow=$(lab "date -d tomorrow +%Y-%m-%d") judge "its next check is tomorrow at $due ($next)" '[[ "$next" == *"$tomorrow $due"* ]]' echo "switched off at the time" due=$(local_in 90) install_test_timer "$due" 60 lab "systemctl start ekk-schedule-test.timer; sync" # A stamp from a previous check, as every classroom has one: last checked # yesterday. The catch-up is decided against it. lab "touch -d yesterday /var/lib/systemd/timers/stamp-ekk-schedule-test.timer" echo " powering the lab off before $due" docker stop -t 20 "$NAME" >/dev/null sleep 150 echo " and on again, after it" "$LAB" reboot >/dev/null 2>&1 # When the lab machine really started (inside a container /proc/uptime is # the host's, so this is asked of docker). booted=$(date -d "$(docker inspect -f '{{.State.StartedAt}}' "$NAME")" +%s) judge "while it was off, nothing ran" '[[ $(lab "grep -c fired $LOG 2>/dev/null || echo 0" | tail -1) -le 1 ]]' for _ in $(seq 1 24); do [[ $(fired) -ge 1 ]] && break; sleep 5; done judge "switched on after $due: the missed check ran after it booted ($(lab "tail -1 $LOG"))" '[[ $(fired) -eq 1 ]]' after=$(( $(lab "awk '{print \$2}' $LOG | head -1") - booted )) judge "$after s after it started: once it was up, within the minute of spread" '(( after >= 0 && after <= 150 ))' sleep 90 judge "and only once: no loop after booting (90 s later, still once)" '[[ $(fired) -eq 1 ]]' lab "systemctl disable --now ekk-schedule-test.timer >/dev/null 2>&1; rm -f /etc/systemd/system/ekk-schedule-test.* $LOG /var/lib/systemd/timers/stamp-ekk-schedule-test.timer; systemctl daemon-reload" echo "reboot only when nobody is using it" docker cp "$ROOT/scripts/classroom-reboot-when-idle" "$NAME:/opt/classroom-reboot-when-idle" >/dev/null lab "chmod 755 /opt/classroom-reboot-when-idle; rm -f /tmp/rebooted" [[ "$IDLE" == /usr/local/bin/* ]] && lab "test -x $IDLE" || IDLE=/opt/classroom-reboot-when-idle cat > /tmp/update-schedule-mouse.py <<'PY' """A mouse made with /dev/uinput: real kernel input events. Moves for N s, then stays.""" import fcntl, os, struct, sys, time UI_SET_EVBIT, UI_SET_RELBIT, UI_SET_KEYBIT = 0x40045564, 0x40045566, 0x40045565 UI_DEV_CREATE, UI_DEV_DESTROY = 0x5501, 0x5502 EV_SYN, EV_KEY, EV_REL, REL_X, BTN_LEFT = 0, 1, 2, 0, 0x110 fd = os.open("/dev/uinput", os.O_WRONLY | os.O_NONBLOCK) for ioctl, value in ((UI_SET_EVBIT, EV_KEY), (UI_SET_KEYBIT, BTN_LEFT), (UI_SET_EVBIT, EV_REL), (UI_SET_RELBIT, REL_X)): fcntl.ioctl(fd, ioctl, value) os.write(fd, struct.pack("80sHHHHi", b"lab-classroom-mouse", 3, 1, 1, 1, 0) + b"\0" * (4 * 64 * 4)) fcntl.ioctl(fd, UI_DEV_CREATE) time.sleep(1) def event(kind, code, value): now = time.time() os.write(fd, struct.pack("llHHi", int(now), int(now % 1 * 1e6), kind, code, value)) end = time.monotonic() + float(sys.argv[1]) while time.monotonic() < end: event(EV_REL, REL_X, 1); event(EV_SYN, 0, 0) time.sleep(2) print("mouse left alone", flush=True) time.sleep(float(sys.argv[2])) fcntl.ioctl(fd, UI_DEV_DESTROY) PY docker cp /tmp/update-schedule-mouse.py "$NAME:/opt/update-schedule-mouse.py" >/dev/null rm -f /tmp/update-schedule-mouse.py docker exec -d "$NAME" python3 /opt/update-schedule-mouse.py 40 90 sleep 2 # The kernel makes the device; on a classroom machine devtmpfs gives it a # node in /dev/input. The lab's /dev is a plain tmpfs, so the node is made # here, from what sysfs says, as devtmpfs would. lab 'for e in /sys/class/input/event*; do [[ "$(cat $e/device/name)" == lab-classroom-mouse ]] || continue IFS=: read -r major minor < $e/dev mknod /dev/input/$(basename $e) c $major $minor 2>/dev/null echo $(basename $e) > /tmp/lab-mouse-node done' sleep 1 docker exec -d "$NAME" sh -c "$IDLE --idle-seconds 25 --check-seconds 2 --reboot-command touch /tmp/rebooted > /tmp/idle.log 2>&1" sleep 35 judge "the mouse moved every 2 s for 40 s: no reboot, against a 25 s limit" "! lab 'test -e /tmp/rebooted'" judge "it was watching real devices ($(lab 'grep -o "watching [0-9]* input devices" /tmp/idle.log'))" \ "lab 'grep -qE \"watching [1-9][0-9]* input devices\" /tmp/idle.log'" sleep 40 judge "left alone for longer than the limit: it rebooted" "lab 'test -e /tmp/rebooted'" lab "pkill -f '[u]pdate-schedule-mouse'; rm -f /dev/input/\$(cat /tmp/lab-mouse-node 2>/dev/null) /tmp/lab-mouse-node /tmp/rebooted /opt/update-schedule-mouse.py /opt/classroom-reboot-when-idle" echo echo "$passed passed, $failed failed" [[ "$failed" -eq 0 ]]