On the laptop the browser jumped behind the disc player the moment it was
opened. 0.2.4 caused that, and the cause is two lines of the stock applet:
main.qml:153 hideActivatedLaunchers: tasks.iconsOnly || ...
main.qml:155 launchInPlace: tasks.iconsOnly && sortingStrategy === 1
For an icons-only bar the first is always true — a launcher is hidden the
instant its application starts — and the second is what puts the running
window back where the launcher was. It only does that at sortingStrategy
1. 0.2.4 set it to 0 to stop teachers dragging the icons about, so the
launcher vanished and the window was appended after the others.
Reproduced in the lab with a real Chrome window before changing anything,
and the bar read off the screen: Wonder Ink, the player, then the
browser. With 1 it is Wonder Ink, the browser, the player, through
opening, a second window, another application opening beside it, and
every close.
The launcher identity was suspected and cleared: only one browser button
appears, because google-chrome.desktop already carries Chrome's own file
name and StartupWMClass for exactly this reason.
THE COST, AND IT IS REAL. Manual sorting is also the only mode in which
MouseHandler.qml accepts a reordering drop, so a teacher who knows to drag
can rearrange the bar again. There is no value that gives both and no
Kiosk key that separates them. classroom-desktop-reset already rebuilds
the bar from the package at every login, so a drag never outlives the
session. docs/testing.md states the trade rather than burying it, and the
configuration escape is written down there too as still open.
tools/lab/order-check is the test this needed and did not have. 0.2.4
passed a full set of configuration assertions while the bar did something
else, so this one opens real application windows and reads the panel
strip as pixels — there is no image library in the lab, so it captures
PPM and names each icon by colour. Six checks, all of them behaviour.
Co-Authored-By: Claude Opus 5 <[email protected]>
154 lines
5.5 KiB
Bash
Executable File
154 lines
5.5 KiB
Bash
Executable File
#!/bin/bash
|
|
# Moving an installer-configured Wi-Fi out of ifupdown's hands.
|
|
#
|
|
# The Bible study laptop had working internet and an applet that said
|
|
# there was no connection, because Debian's installer had written the
|
|
# Wi-Fi into /etc/network/interfaces and NetworkManager marks an
|
|
# interface named there UNMANAGED. This is the parsing half of the fix:
|
|
# find that stanza, get the network out of it, and comment out exactly
|
|
# it and nothing else.
|
|
set -uo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
PYTHONPATH="$ROOT/lib" python3 - "$ROOT" <<'PY'
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
root = Path(sys.argv[1])
|
|
sys.path.insert(0, str(root / "lib"))
|
|
|
|
from classroom_network import comment_out, credentials, parse, wireless
|
|
|
|
passed = failed = 0
|
|
|
|
|
|
def check(label, condition, detail=""):
|
|
global passed, failed
|
|
if condition:
|
|
print(f" ok {label}")
|
|
passed += 1
|
|
else:
|
|
print(f" FAIL {label}")
|
|
if detail:
|
|
print(f" {detail}")
|
|
failed += 1
|
|
|
|
|
|
# The shape Debian's installer writes when it is given a wireless network
|
|
# during installation. Measured against the laptop: /etc/network/interfaces
|
|
# is 0600 root:root, wpa-ssid and wpa-psk are inline, and the
|
|
# wpa_supplicant it starts has no -c config file, which is what tells you
|
|
# ifupdown fed it these values rather than a wpa_supplicant.conf.
|
|
LAPTOP = """# This file describes the network interfaces available on your system
|
|
source /etc/network/interfaces.d/*
|
|
|
|
auto lo
|
|
iface lo inet loopback
|
|
|
|
allow-hotplug wlp2s0
|
|
iface wlp2s0 inet dhcp
|
|
\twpa-ssid Sanctuary
|
|
\twpa-psk a-real-secret
|
|
"""
|
|
|
|
# A classroom tower: static Ethernet an administrator put under
|
|
# ifupdown's control on purpose. Nothing here may touch it.
|
|
TOWER = """source /etc/network/interfaces.d/*
|
|
|
|
auto lo
|
|
iface lo inet loopback
|
|
|
|
auto enp1s0
|
|
iface enp1s0 inet static
|
|
\taddress 10.40.0.55/24
|
|
\tgateway 10.40.0.1
|
|
"""
|
|
|
|
|
|
def wireless_names(*names):
|
|
return lambda device: device in names
|
|
|
|
|
|
print("1. the laptop's own file")
|
|
stanzas = parse(LAPTOP)
|
|
check("every iface stanza is found",
|
|
[s["name"] for s in stanzas] == ["lo", "wlp2s0"],
|
|
str([s["name"] for s in stanzas]))
|
|
found = wireless(stanzas, wireless_names("wlp2s0"))
|
|
check("the wireless one is picked out", len(found) == 1)
|
|
check("and it is not the loopback", found and found[0]["name"] == "wlp2s0")
|
|
|
|
ssid, psk = credentials(found[0])
|
|
check("the network name is read", ssid == "Sanctuary", repr(ssid))
|
|
check("and the password", psk == "a-real-secret", repr(psk))
|
|
|
|
print("2. a tower is left completely alone")
|
|
tower = parse(TOWER)
|
|
check("its Ethernet is not mistaken for wireless",
|
|
wireless(tower, wireless_names("wlp2s0")) == [])
|
|
check("even though it has a static stanza ifupdown owns",
|
|
[s["name"] for s in tower] == ["lo", "enp1s0"])
|
|
# managed=false stays because of this machine, so nothing may change here.
|
|
check("commenting out is never reached for it",
|
|
not wireless(tower, lambda d: False))
|
|
|
|
print("3. only the wireless stanza is commented out")
|
|
after = comment_out(LAPTOP, found[0])
|
|
check("the iface line is commented",
|
|
"# ekklesia-classroom: iface wlp2s0" in after, after)
|
|
check("its options are commented with it",
|
|
"# ekklesia-classroom: \twpa-ssid" in after)
|
|
check("the allow-hotplug above it goes too, or ifupdown asks for an "
|
|
"interface with no stanza",
|
|
"# ekklesia-classroom: allow-hotplug wlp2s0" in after)
|
|
check("the loopback is untouched",
|
|
"\niface lo inet loopback\n" in after)
|
|
check("and so is the source line",
|
|
"\nsource /etc/network/interfaces.d/*\n" in after)
|
|
check("the password is still in the file, not deleted",
|
|
"a-real-secret" in after,
|
|
"the original must remain recoverable until rollback is impossible")
|
|
|
|
print("4. doing it twice changes nothing the second time")
|
|
twice = comment_out(after, parse(after)[0] if parse(after) else found[0])
|
|
check("already-commented lines are not commented again",
|
|
"# ekklesia-classroom: # ekklesia-classroom:" not in twice)
|
|
|
|
print("5. a stanza this must refuse to move")
|
|
# Pointing at a wpa_supplicant.conf means the credentials are not in this
|
|
# file, so there is nothing here to recreate and guessing would be wrong.
|
|
CONF = """allow-hotplug wlan0
|
|
iface wlan0 inet dhcp
|
|
\twpa-conf /etc/wpa_supplicant/wpa_supplicant.conf
|
|
"""
|
|
stanza = wireless(parse(CONF), wireless_names("wlan0"))[0]
|
|
check("a wpa-conf stanza yields no SSID, so the caller stops",
|
|
credentials(stanza) == (None, None), str(credentials(stanza)))
|
|
|
|
print("6. a renamed or absent device is still recognised")
|
|
# The machine is asked whether a device is wireless, but a wpa-ssid is
|
|
# evidence on its own — a device that has been renamed between kernels
|
|
# would otherwise be invisible.
|
|
check("a wpa-ssid alone is enough",
|
|
len(wireless(parse(LAPTOP), lambda d: False)) == 1)
|
|
|
|
print("7. the command itself")
|
|
wifi = (root / "scripts/classroom-wifi").read_text()
|
|
check("--adopt exists and has a dry run", "--adopt" in wifi and "--dry-run" in wifi)
|
|
check("it keeps the original file before changing it",
|
|
"before-ekklesia-classroom" in wifi)
|
|
check("it verifies the room is really online, not just that NM says so",
|
|
"curl" in wifi and "connectivity" in wifi)
|
|
check("it rolls back to ifupdown if the network does not come back",
|
|
"rolling back" in wifi and "ifup" in wifi)
|
|
check("it never prints the password",
|
|
"psk}" not in wifi and "{psk" not in wifi)
|
|
check("the connection it creates is still a system one",
|
|
'"connection.permissions"' not in wifi)
|
|
|
|
print()
|
|
print(f"{passed} passed, {failed} failed")
|
|
raise SystemExit(1 if failed else 0)
|
|
PY
|