diff --git a/config/plasma/applications/ekklesia-admin-terminal.desktop b/config/plasma/applications/ekklesia-admin-terminal.desktop index 0b22b70..8b5f745 100644 --- a/config/plasma/applications/ekklesia-admin-terminal.desktop +++ b/config/plasma/applications/ekklesia-admin-terminal.desktop @@ -1,6 +1,6 @@ [Desktop Entry] Type=Application -Name=Ekklesia maintenance terminal +Name=Maintenance terminal Comment=For the technical administrator, standing at the machine Exec=/usr/local/bin/classroom-admin-terminal Icon=utilities-terminal diff --git a/config/plasma/xdg/kdeglobals b/config/plasma/xdg/kdeglobals index 7bcfda5..b1bb5f3 100644 --- a/config/plasma/xdg/kdeglobals +++ b/config/plasma/xdg/kdeglobals @@ -38,6 +38,27 @@ action/help_contents=false # that has no settings application anyway; the lock belongs on the # restrictions above, not on the palette. [General] +# Sized for a television, not a desk. +# +# Everything Plasma draws is measured in grid units, and a grid unit is +# the height of this font: the notification card's width, its padding, +# its icon and its buttons all come from here. There is no setting that +# makes a notification larger on its own, and the supported lever is this +# one — which is also the reason it is used rather than a replacement +# notification plasmoid. +# +# Measured at 100% in the lab: Breeze's 10 point gives a card about 350 +# by 110 pixels with title text a volunteer at the back of the room +# cannot read; 15 point gives about 410 by 145 with a title that carries. +# The bar does not change, because its height is fixed and the clock +# sizes itself to it. +# +# At 200% the display scale doubles all of it again, so a 4K television +# gets the same card at twice the size without a second number. +font=Noto Sans,15,-1,5,50,0,0,0,0,0 +smallestReadableFont=Noto Sans,13,-1,5,50,0,0,0,0,0 +menuFont=Noto Sans,15,-1,5,50,0,0,0,0,0 +toolBarFont=Noto Sans,15,-1,5,50,0,0,0,0,0 ColorScheme=EkklesiaDark AccentColor=79,179,168 # The teacher's own words are not decoration: keep text rendering plain diff --git a/docs/classroom-v2-experiment.md b/docs/classroom-v2-experiment.md index 0570912..2fa43d4 100644 --- a/docs/classroom-v2-experiment.md +++ b/docs/classroom-v2-experiment.md @@ -1201,6 +1201,98 @@ worked: it used `iptables`, which is not installed in the lab image, and it probed with `/dev/tcp` under `sh`, which is a bash feature and fails under dash whatever the network is doing. Both were reported as success. +## A morning in the rooms: 0.1.50 + +Four things a television found that a container had not. + +### "Parent Call has not been set up yet", on a room that was + +The card came up checking with the sanctuary and then said the room had +never been set up. Nothing about the room was wrong. Read from the church +broker, read-only: + +``` +classroom/parent-call/coordinator {"online": true, ...} +classroom/parent-call/pending {...} +classroom/parent-call/mode — absent +``` + +The coordinator's own source says why. It publishes the retained mode +only in answer to an operator writing to `…/mode/set`, and on this +broker nobody ever has. The classroom waited 1.5 seconds for a retained +message that will never exist, and then told the teacher the room was +unconfigured. + +So a missing mode is now read as what it is: the coordinator's own +default, which the sanctuary end knows and this end does not. The room +offers the call and lets the coordinator answer. If the true mode is off, +the coordinator refuses in its own words through the status topic, which +the card already shows — a wrong no costs a teacher the call, a wrong yes +costs a round trip and tells the truth at the end of it. + +The distinction the card has to keep is untouched. A broker that is not +configured at all still says so, a broker that cannot be reached still +says that, and a coordinator that is down still names the sanctuary. +"Undecided" is now a third answer, with no message, which is why the card +shows nothing at all until the retained topics have had their chance. + +Proved on a rig whose broker was made to match the church's exactly — +coordinator and pending retained, mode deleted. The card came up ready, +and a call went through to the sanctuary mock: `Ruth Alvarez`, `Nursery`. + +### The card opens where its button is + +Centred, on a classroom television, is a long way from the button in the +bottom-left corner. A Wayland client cannot place its own window, so the +card is anchored rather than positioned: layer shell, bottom and left +edges, margins in logical pixels. The compositor does the arithmetic, so +it is correct at 1920×1080 at 100% and at 3840×2160 at 200% with no +coordinates written down anywhere. Both were photographed. + +The classroom has drawn the disc player's cover this way under KWin for +months, so the mechanism is not new here. It is optional: if the library +is missing the card still opens, centred, exactly as before. + +**The one thing the lab cannot answer is whether a teacher can type into +it.** The entry takes focus and shows a caret, which is KWin giving the +layer surface the keyboard. Actually typing has never been testable in +this lab — the tool that tries closes the card — so the first bench test +on this build is a name typed into Parent Call with a real keyboard. + +### Notifications for a room, not a desk + +Everything Plasma draws is measured in grid units, and a grid unit is the +height of the general font. There is no setting that makes a notification +larger on its own, so the font is the lever — and it is the supported +one, which matters after the shell package that cost the panel. + +Measured at 100%: Breeze's 10 point gives a card about 350×110 with a +title a volunteer at the back cannot read; 15 point gives about 410×145 +with a title that carries. Text, icon, padding and buttons all move +together because they are all grid units. The bar does not change: its +height is fixed and the clock sizes itself to it. + +At 200% the display scale doubles all of it again. No chime, unchanged: +`AudioFeedback` is off in `plasmaparc` and the applet's own setting is +off in the layout. + +### The room does not introduce itself + +"Ekklesia Classroom" is gone from everything a teacher sees. The +notification card is from `Classroom`, the disc player's notifications +are from `DVD`, the maintenance terminal is a `Maintenance terminal`. +Nineteen visible names are checked by the test suite now, and the product +name stays where it belongs: package names, units, paths and topics. + +### And the lab, again + +Four rooms named after container ids are sitting on the church broker as +retained status messages. The fence was applied when each container +started and lost the moment it was restarted, because routes do not +survive that — and `lab reboot` is in every scenario. It re-fences now +and refuses to continue if either church network answers. The four +retained topics are the user's to clear; the commands are in the report. + ## Unresolved risks In the order I would worry about them. @@ -1275,7 +1367,13 @@ In the order I would worry about them. updates at all, and `classroom-migrate` moves every upgraded machine onto that channel — deliberately, because stable and canary would both hand a Plasma room the sway build back. -17. **Ctrl+Alt+T is still registered.** Konsole's own entry declares it +17. **Nobody has typed into the anchored Parent Call card.** Layer shell + gives it the keyboard — the entry takes focus and shows a caret — but + keyboard injection has never worked in this lab, so the first thing + to do on the bench is type a name into it. If it cannot be typed + into, removing gtk-layer-shell from the package restores the centred + card with no other change. +18. **Ctrl+Alt+T is still registered.** Konsole's own entry declares it and the registration survives being shadowed. It is inert, because Konsole refuses to open a shell under the classroom's `shell_access=false` — measured — but it rests on that one diff --git a/docs/screenshots/README.md b/docs/screenshots/README.md index dd1ffeb..b7bf96b 100644 --- a/docs/screenshots/README.md +++ b/docs/screenshots/README.md @@ -101,6 +101,16 @@ container lab. See [../classroom-v2-experiment.md](../classroom-v2-experiment.md | `pc-unreachable`, `pc-not-configured` | Parent Call telling a broker it cannot reach from one that was never set up | | `dvd-absent-bar` | the bar on a machine with no optical drive | +## A morning in the rooms + +| | | +| --- | --- | +| `pc-no-mode` | Parent Call ready against a broker with no retained mode — the state that used to say the room was never set up | +| `pc-bottom-left`, `pc-200-package` | the card anchored bottom-left at 100% and at 200% | +| `notif-package-100`, `notif-package-200` | the notification card at both scales, from `Classroom` | +| `notif-13pt`, `notif-15pt` | the two font sizes that were weighed for it | +| `pc-typed-layer` | the card gone: the lab's keyboard tool closes it, which is why typing is still a bench test | + ## Failures, kept | | | diff --git a/docs/screenshots/bar-13pt.png b/docs/screenshots/bar-13pt.png new file mode 100644 index 0000000..7699e85 Binary files /dev/null and b/docs/screenshots/bar-13pt.png differ diff --git a/docs/screenshots/notif-13pt-full.png b/docs/screenshots/notif-13pt-full.png new file mode 100644 index 0000000..cc0ba2a Binary files /dev/null and b/docs/screenshots/notif-13pt-full.png differ diff --git a/docs/screenshots/notif-13pt.png b/docs/screenshots/notif-13pt.png new file mode 100644 index 0000000..1cd5e80 Binary files /dev/null and b/docs/screenshots/notif-13pt.png differ diff --git a/docs/screenshots/notif-15pt.png b/docs/screenshots/notif-15pt.png new file mode 100644 index 0000000..df64d0e Binary files /dev/null and b/docs/screenshots/notif-15pt.png differ diff --git a/docs/screenshots/notif-package-100.png b/docs/screenshots/notif-package-100.png new file mode 100644 index 0000000..10aa763 Binary files /dev/null and b/docs/screenshots/notif-package-100.png differ diff --git a/docs/screenshots/notif-package-200.png b/docs/screenshots/notif-package-200.png new file mode 100644 index 0000000..0ec87ad Binary files /dev/null and b/docs/screenshots/notif-package-200.png differ diff --git a/docs/screenshots/pc-200-package.png b/docs/screenshots/pc-200-package.png new file mode 100644 index 0000000..5919ee6 Binary files /dev/null and b/docs/screenshots/pc-200-package.png differ diff --git a/docs/screenshots/pc-bottom-left-200.png b/docs/screenshots/pc-bottom-left-200.png new file mode 100644 index 0000000..a8bf9fd Binary files /dev/null and b/docs/screenshots/pc-bottom-left-200.png differ diff --git a/docs/screenshots/pc-bottom-left.png b/docs/screenshots/pc-bottom-left.png new file mode 100644 index 0000000..3c550c1 Binary files /dev/null and b/docs/screenshots/pc-bottom-left.png differ diff --git a/docs/screenshots/pc-no-mode.png b/docs/screenshots/pc-no-mode.png new file mode 100644 index 0000000..548f06f Binary files /dev/null and b/docs/screenshots/pc-no-mode.png differ diff --git a/docs/screenshots/pc-sent-no-mode.png b/docs/screenshots/pc-sent-no-mode.png new file mode 100644 index 0000000..7ddf2b1 Binary files /dev/null and b/docs/screenshots/pc-sent-no-mode.png differ diff --git a/docs/screenshots/pc-typed-layer.png b/docs/screenshots/pc-typed-layer.png new file mode 100644 index 0000000..80fca1a Binary files /dev/null and b/docs/screenshots/pc-typed-layer.png differ diff --git a/lib/classroom_parent_call.py b/lib/classroom_parent_call.py index 04612ba..74bc4cb 100644 --- a/lib/classroom_parent_call.py +++ b/lib/classroom_parent_call.py @@ -287,12 +287,18 @@ def broker_message(why=None): return BROKER_WORDING.get(why, BROKER_UNREACHABLE) -def availability(mode, coordinator_online, broker_reachable=True, why=None): +def availability(mode, coordinator_online, broker_reachable=True, why=None, + assume_default=False): """Whether a teacher may send a call now, and what to say if not. Returns (can_send, message). The wording for an off mode is the coordinator's own, so the room says the same thing the sanctuary - would. + would. A message of None means the question is not answered yet + rather than answered no. + + assume_default is the caller saying the retained topics have had + their chance. Until then a missing mode is undecided; after it, a + missing mode is the coordinator's own default — see below. """ if not broker_reachable: return False, broker_message(why) @@ -304,6 +310,23 @@ def availability(mode, coordinator_online, broker_reachable=True, why=None): return False, WORDING[UNAVAILABLE] if mode is None: - return False, "Parent Call has not been set up yet" + # No retained mode on the broker at all. + # + # This is not a room that was never set up, and treating it as + # one is what every classroom did: the card said "Parent Call has + # not been set up yet" on machines whose broker, room and + # coordinator were all exactly right. The coordinator publishes + # the retained mode only in answer to an operator writing to + # …/mode/set, and on this church's broker nobody ever has, so the + # topic does not exist. Read from the real broker: …/coordinator + # and …/pending are retained, …/mode is absent. + # + # So a mode nobody has set is the coordinator's own DEFAULT_MODE, + # and the sanctuary is the end that knows it. The room offers the + # call and lets the coordinator answer; if the true mode is off it + # refuses, in its own words, through the status topic. A wrong no + # here costs a teacher the call. A wrong yes costs a round trip + # and tells the truth at the end of it. + return (True, None) if assume_default else (False, None) return True, None diff --git a/scripts/classroom-dvd b/scripts/classroom-dvd index 5ffc653..503656b 100755 --- a/scripts/classroom-dvd +++ b/scripts/classroom-dvd @@ -30,7 +30,7 @@ def notify(title, message, urgency="normal"): subprocess.Popen( [ "notify-send", - "--app-name=Ekklesia Classroom", + "--app-name=DVD", f"--urgency={urgency}", "--", title, diff --git a/scripts/classroom-notify b/scripts/classroom-notify index 5785340..9b1194a 100755 --- a/scripts/classroom-notify +++ b/scripts/classroom-notify @@ -208,7 +208,7 @@ if [[ "$dry_run" == true ]]; then fi "${runner[@]}" notify-send \ - --app-name="Ekklesia Classroom" \ + --app-name="Classroom" \ --urgency="$urgency" \ -- \ "$title" "$body" diff --git a/scripts/classroom-parent-call b/scripts/classroom-parent-call index a41b63f..2ee4562 100755 --- a/scripts/classroom-parent-call +++ b/scripts/classroom-parent-call @@ -39,6 +39,37 @@ gi.require_version("Gdk", "3.0") from gi.repository import Gdk, GLib, Gtk # noqa: E402 +# Where the card sits on the screen. +# +# A Wayland client cannot place its own window, and the card used to be +# centred because KWin centres everything. On a classroom television that +# puts it in the middle of whatever the room is watching, a long way from +# the button that opened it. Layer shell is the compositor's own answer: +# the surface is anchored to an edge and the compositor does the +# arithmetic, so bottom-left is bottom-left at 1080p and at 4K, at 100% +# and at 200%, with no coordinates written down anywhere. +# +# The classroom already draws the disc player's cover and control bar +# this way under KWin, so the mechanism is one this room has run for +# months rather than a new dependency. +# +# Optional on purpose. If the library is missing the card still opens, +# centred, exactly as it did before: a teacher can always reach it. +try: + gi.require_version("GtkLayerShell", "0.1") + from gi.repository import GtkLayerShell # noqa: E402 +except (ImportError, ValueError): + GtkLayerShell = None + +# Logical pixels, so they mean the same thing at every scale. +# +# The bar is 76 logical pixels tall and reserves that space, so the +# compositor already keeps a bottom-anchored surface clear of it; this +# gap is the breathing room above it. The left margin lines the card up +# near the launcher that opened it rather than hard against the edge. +CARD_GAP_BOTTOM = 16 +CARD_GAP_LEFT = 16 + sys.path.insert(0, "/usr/local/lib/ekklesia-classroom") sys.path.insert( 0, @@ -395,11 +426,7 @@ class ParentCall(Gtk.Window): self.set_titlebar(Gtk.Box()) self.set_resizable(False) self.set_default_size(460, -1) - # Deliberately not set_position(): a Wayland client cannot place - # itself, and asking for the centre put the card in the top-left - # corner instead. KWin centres it, because kwinrc already asks for - # every window to be centred — no rule of its own is needed, and - # the one that was written for it turned out to be doing nothing. + self._anchor_bottom_left() self.set_type_hint(Gdk.WindowTypeHint.DIALOG) # Belt and braces with the window rule: whichever of the two KWin # honours, the card must not end up behind the lesson a teacher @@ -499,6 +526,44 @@ class ParentCall(Gtk.Window): self.get_screen(), css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) + def _anchor_bottom_left(self): + """Put the card in the corner its launcher is in. + + Anchored rather than positioned: the compositor is told which two + edges to hold the surface against and works the rest out itself, + so this is right on a 1080p television at 100% and on a 4K one at + 200% without either number appearing here. + + Keyboard interactivity is the part that matters and the part that + has to be asked for: a teacher types a child's name into this. + """ + if GtkLayerShell is None: + return + + try: + GtkLayerShell.init_for_window(self) + # TOP, not OVERLAY. The disc player's cover is an overlay and + # is meant to be in front of everything including this; a + # teacher fetching a parent during a film is not a reason to + # put a text box over the film's own controls. + GtkLayerShell.set_layer(self, GtkLayerShell.Layer.TOP) + GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.BOTTOM, True) + GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.LEFT, True) + GtkLayerShell.set_margin( + self, GtkLayerShell.Edge.BOTTOM, CARD_GAP_BOTTOM) + GtkLayerShell.set_margin( + self, GtkLayerShell.Edge.LEFT, CARD_GAP_LEFT) + # Reserve nothing. The card is a moment, not furniture, and a + # surface that reserved space would push the lesson around + # every time somebody fetched a parent. + GtkLayerShell.set_exclusive_zone(self, 0) + GtkLayerShell.set_keyboard_interactivity(self, True) + except Exception as error: # noqa: BLE001 + # Any failure here is cosmetic. A centred card a teacher can + # type into beats a perfectly placed one that never opened. + print(f"classroom-parent-call: layer shell unavailable: {error}", + file=sys.stderr) + def _set_status(self, text, tone=None, detail=None): style = self.status.get_style_context() @@ -520,7 +585,8 @@ class ParentCall(Gtk.Window): and self.broker_reachable and contract.clean_child(self.entry.get_text()) is not None and contract.availability( - self.mode, self.coordinator_online, self.broker_reachable + self.mode, self.coordinator_online, self.broker_reachable, + assume_default=self.settled, )[0] ) self.primary.set_sensitive(bool(ready)) @@ -585,7 +651,8 @@ class ParentCall(Gtk.Window): return can, why = contract.availability( - self.mode, self.coordinator_online, self.broker_reachable + self.mode, self.coordinator_online, self.broker_reachable, + assume_default=final, ) if can: @@ -642,8 +709,11 @@ class ParentCall(Gtk.Window): self._set_status("Enter the child's name", tone="bad") return + # The teacher is pressing the button, so the retained topics have + # long since had their chance. can, why = contract.availability( - self.mode, self.coordinator_online, self.broker_reachable + self.mode, self.coordinator_online, self.broker_reachable, + assume_default=True, ) if not can: @@ -673,11 +743,16 @@ class ParentCall(Gtk.Window): def _retained_deadline(self): # Retained topics arrive at once or not at all. Now that both have - # had their chance, a negative is safe to show. + # had their chance, a negative is safe to show, and a mode nobody + # has ever set is the coordinator's default rather than a room + # that was never configured. self._settle(final=True) if not self.settled: - self._unavailable("Parent Call has not been set up yet") + # Only reachable if nothing at all came back from the + # sanctuary end, which the wording should say rather than + # blaming this room's setup. + self._unavailable("No answer from the sanctuary") return False diff --git a/tests/test-classroom-notify.sh b/tests/test-classroom-notify.sh index 57c35df..e3f03b0 100755 --- a/tests/test-classroom-notify.sh +++ b/tests/test-classroom-notify.sh @@ -152,7 +152,7 @@ log="$(cat "$NOTIFY_LOG")" check "runtime dir taken from the compositor" "XDG_RUNTIME_DIR=$RUNTIME" "$log" check "bus address taken from the compositor" "DBUS_SESSION_BUS_ADDRESS=unix:path=$RUNTIME/bus" "$log" check "wayland display from the compositor" "WAYLAND_DISPLAY=wayland-7" "$log" -check "styling preserved" "--app-name=Ekklesia Classroom" "$log" +check "styling preserved" "--app-name=Classroom" "$log" check "default sample title" "DVD Ready" "$log" check "default sample body" "A disc is ready to play." "$log" @@ -183,7 +183,7 @@ echo "4. root delivers as the classroom user" FAKE_CURRENT_UID=0 out="$(run)" check "runuser targets classroom" "runuser -u classroom --" "$(cat "$RUNUSER_LOG")" -check "notification still sent" "--app-name=Ekklesia Classroom" "$(cat "$NOTIFY_LOG")" +check "notification still sent" "--app-name=Classroom" "$(cat "$NOTIFY_LOG")" FAKE_CURRENT_UID=4000 # ------------------------------------ 5. unprivileged admin is told how @@ -276,7 +276,7 @@ export XDG_RUNTIME_DIR="$RUNTIME" out="$(run)" check "reports delivery to this session" "Sent normal notification to this session" "$out" -check "notification was sent" "--app-name=Ekklesia Classroom" "$(cat "$NOTIFY_LOG")" +check "notification was sent" "--app-name=Classroom" "$(cat "$NOTIFY_LOG")" check "inherits the session display" "WAYLAND_DISPLAY=wayland-4" "$(cat "$NOTIFY_LOG")" check_absent "did not go through runuser" "runuser" "$(cat "$RUNUSER_LOG")" diff --git a/tests/test-classroom-parent-call.sh b/tests/test-classroom-parent-call.sh index 20df31a..cf4eb5b 100755 --- a/tests/test-classroom-parent-call.sh +++ b/tests/test-classroom-parent-call.sh @@ -154,9 +154,31 @@ check("no broker stops it", not can) check("and says the network, not the sanctuary", "network" in why, why) +# No retained mode on the broker. The coordinator publishes one only when +# an operator writes to …/mode/set, and on the church's broker nobody +# ever has — so every room was told it had never been set up. can, why = availability(None, True) -check("a broker with no retained mode stops it", not can) -check("and says it is not set up", "set up" in why, why) +check("an undecided mode is not yet a yes", not can) +check("and is not a refusal either", why is None, why) + +can, why = availability(None, True, assume_default=True) +check("once the retained topics have had their chance, the call goes", + can) +check("with nothing to say about it", why is None, why) + +can, why = availability(MODE_OFF, True, assume_default=True) +check("a mode that really is off still stops it", not can) +check("in the sanctuary's own words", why == WORDING[UNAVAILABLE], why) + +can, why = availability(None, False, assume_default=True) +check("and a coordinator that is down still stops it", not can) +check("naming the sanctuary, not this room", "sanctuary" in why, why) + +can, why = availability(None, True, broker_reachable=False, + why="not configured") +check("a room with no broker configured is still its own answer", not can) +check("and says this room, not the network", + "no church network set up" in why, why) print() print(f"{passed} passed, {failed} failed") diff --git a/tests/test-deployment-support.sh b/tests/test-deployment-support.sh index 7a5c29c..67831ab 100755 --- a/tests/test-deployment-support.sh +++ b/tests/test-deployment-support.sh @@ -314,6 +314,41 @@ check("and cannot ask to", chrome_policy.get("NotificationsAllowedForUrls") == [] and chrome_policy.get("NotificationsBlockedForUrls") == ["*"]) +print() +print("7c-i. the room does not introduce itself") +# A teacher does not need the name of the software. Every visible name is +# the thing it does — Parent Call, DVD, Browser, Classroom — and the +# product name stays where it belongs: package names, unit names, paths +# and topics, none of which anybody in the room reads. +visible = {} + +for entry in sorted((root / "config/plasma").rglob("*.desktop")): + for line in entry.read_text().splitlines(): + if line.split("=", 1)[0] in ("Name", "GenericName", "Comment"): + visible[f"{entry.name}:{line}"] = line + +for script in sorted((root / "scripts").iterdir()): + if not script.is_file(): + continue + for line in script.read_text().splitlines(): + if "--app-name" in line: + visible[f"{script.name}:{line.strip()}"] = line + +check("there are visible names to check", len(visible) > 10) + +offenders = [where for where, line in visible.items() if "Ekklesia" in line] + +if offenders: + for where in offenders: + print(f" {where}") + +check(f"none of them says the product name ({len(visible)} checked)", + not offenders) + +check("the notification card is from the room", + '--app-name="Classroom"' + in (root / "scripts/classroom-notify").read_text()) + print() print("7c-ii. the v2 channel is its own") v2 = (root / "update_classroom_v2.sh").read_text() diff --git a/tools/lab/lab b/tools/lab/lab index ca0facf..ddc5c42 100755 --- a/tools/lab/lab +++ b/tools/lab/lab @@ -135,6 +135,22 @@ reboot) done docker exec "$NAME" ln -sf /usr/bin/qdbus6 /usr/local/bin/qdbus + # The fence again. Routes do not survive a container restart, and the + # lab reached the church broker through exactly that gap: four rooms + # named after container ids on the real broker, from sessions that + # had been fenced when they started and were not after a reboot. + docker exec "$NAME" sh -c ' + for net in 10.30.0.0/16 10.40.0.0/16; do + ip route replace blackhole "$net" || exit 1 + done' || echo " WARNING: could not fence the church networks" + + for target in 10.40.0.2/1883 10.30.0.2/1025; do + if docker exec "$NAME" timeout 4 bash -c "exec 3<>/dev/tcp/${target}" 2>/dev/null; then + echo "REFUSING TO CONTINUE: ${target} is reachable from the lab" >&2 + exit 1 + fi + done + # 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 diff --git a/tools/lab/parent-call-rig b/tools/lab/parent-call-rig index a03aaf0..a9f0899 100755 --- a/tools/lab/parent-call-rig +++ b/tools/lab/parent-call-rig @@ -109,8 +109,11 @@ import pathlib p = pathlib.Path("/etc/ekklesia-classroom/device.conf") t = p.read_text() t = t.replace("MQTT_ENABLED=false", "MQTT_ENABLED=true") -if "MQTT_HOST=127.0.0.1" not in t: - t = t.replace("MQTT_HOST=", "MQTT_HOST=127.0.0.1", 1) +# The whole line, not a prefix. Replacing the prefix left +# MQTT_HOST=127.0.0.1broker.example.invalid on any machine whose broker +# was already set — which, since the package started shipping a default, +# is every machine. +t = re.sub(r"^MQTT_HOST=.*$", "MQTT_HOST=127.0.0.1", t, count=1, flags=re.M) if "\nROOM=" not in t: t += "\n# Lab only.\nROOM=nursery\n" p.write_text(t)