Little Waders took the Plasma build and reported what a container cannot. A disc player on a machine with no optical drive, a clock nobody at the back of the room could read, a browser at half the size of the panel around it, and Parent Call with no broker to call. The broker is the same for every classroom, so it is a default now and not something to fill in room by room. classroom-migrate brings an existing room to the same place without touching its name, its username or its password, and Parent Call distinguishes a broker it cannot reach from one that was never set up: those send somebody to look at completely different things. The disc player's detection was right and ran too late — alongside the session rather than before it, and the panel is built from the layout the moment a session starts. It runs before the display manager now, and again from udev when a drive is plugged in. UI_SCALE had a ceiling of 1.5, which cannot express a 4K television; a clock pinned at 15 points, which is the same size at every scale; and a browser that was never told the number at all. All three are fixed and the room doubles together at 200%. This also brings 0.1.47's work into the repository, where it had only ever existed in a build directory under /tmp: the hidden display dialog, its authenticated writer, the polkit action, and the church's own wallpaper. One piece of it is deliberately left out. The custom volume display needed a shell package of our own, and with one selected plasmashell builds no panel at all — measured twice, with that build's own files, on a classroom that deletes its panel configuration at every login by design. The lab connected to the real church broker the moment the package defaulted to it. Nothing was left behind, which was checked, but the guard that should have stopped it had never worked: it used iptables, which the lab image does not have, and probed with /dev/tcp under dash, where that always fails. Both reported success. Co-Authored-By: Claude Opus 5 <[email protected]>
121 lines
3.9 KiB
Python
Executable File
121 lines
3.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
"""Match the classroom's panel to the machine it is on.
|
|
|
|
There is exactly one thing about the bar that depends on hardware: a room
|
|
with no optical drive should not be offered a disc player. This edits the
|
|
one marked line in the installed layout, and nothing else.
|
|
|
|
Run before the display manager by ekklesia-classroom-layout.service, and
|
|
again by udev when a drive is plugged in or pulled out. That ordering is
|
|
the whole point: the panel is built from this layout the moment a session
|
|
starts, and a room that is told about its hardware afterwards keeps the
|
|
wrong bar until the next login. It was being left to the provisioning
|
|
service, which runs at boot alongside the session rather than before it.
|
|
|
|
It writes the *layout*, not the running panel. Editing
|
|
plasma-org.kde.plasma.desktop-appletsrc underneath a live plasmashell was
|
|
tried and it destroyed the panel: the shell owns that file and rewrites
|
|
it, so an edit races with it. The layout is a file the package owns and
|
|
the shell only reads, and classroom-desktop-reset makes the shell read it
|
|
again at the next login.
|
|
|
|
DVD_ENABLED is gone. A drive is something the machine can be asked about,
|
|
and asking is more reliable than remembering to edit a file when a
|
|
computer moves between rooms.
|
|
"""
|
|
|
|
import glob
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
LAYOUT = (
|
|
"/usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop"
|
|
"/contents/layouts/org.kde.plasma.desktop-layout.js"
|
|
)
|
|
|
|
MARKER = "/* CLASSROOM-DVD-LAUNCHER */"
|
|
|
|
# The kernel names optical drives sr0, sr1, ... regardless of bus. Both
|
|
# views are checked: /dev is what anything opening the drive will use, and
|
|
# /sys/block is the kernel's own list, which cannot be left behind by a
|
|
# device node somebody or something created by hand.
|
|
DRIVE_GLOB = "/dev/sr[0-9]*"
|
|
SYSFS_GLOB = "/sys/block/sr[0-9]*"
|
|
|
|
|
|
def drives():
|
|
return sorted(glob.glob(DRIVE_GLOB)), sorted(glob.glob(SYSFS_GLOB))
|
|
|
|
|
|
def has_optical_drive():
|
|
nodes, blocks = drives()
|
|
|
|
# Said out loud every time. When a room shows a disc player it has no
|
|
# drive for, or hides one it has, this line in the journal is the
|
|
# difference between an answer and an afternoon.
|
|
print("classroom-desktop-layout: /dev "
|
|
f"[{', '.join(nodes) or 'none'}], /sys/block "
|
|
f"[{', '.join(blocks) or 'none'}]")
|
|
|
|
return bool(nodes and blocks)
|
|
|
|
|
|
def rewrite(text, wanted):
|
|
"""Comment the DVD launcher in or out, idempotently."""
|
|
line = re.compile(
|
|
r"^(\s*)(//\s*)?(" + re.escape(MARKER) + r'\s*"applications:ekklesia-dvd\.desktop")',
|
|
re.M,
|
|
)
|
|
|
|
def swap(match):
|
|
indent, _, body = match.groups()
|
|
return f"{indent}{'' if wanted else '// '}{body}"
|
|
|
|
return line.sub(swap, text)
|
|
|
|
|
|
def main():
|
|
path = sys.argv[1] if len(sys.argv) > 1 else LAYOUT
|
|
|
|
try:
|
|
with open(path, "r", encoding="utf-8") as handle:
|
|
before = handle.read()
|
|
except OSError as error:
|
|
print(f"classroom-desktop-layout: {error}", file=sys.stderr)
|
|
return 0
|
|
|
|
if MARKER not in before:
|
|
print("classroom-desktop-layout: no DVD marker in the layout",
|
|
file=sys.stderr)
|
|
return 0
|
|
|
|
wanted = has_optical_drive()
|
|
after = rewrite(before, wanted)
|
|
|
|
if after == before:
|
|
print(f"classroom-desktop-layout: DVD launcher already "
|
|
f"{'present' if wanted else 'absent'}")
|
|
return 0
|
|
|
|
# Replace the file rather than edit in place, so a full disk or a
|
|
# crash leaves the old layout intact rather than half of a new one.
|
|
temporary = f"{path}.new"
|
|
|
|
try:
|
|
with open(temporary, "w", encoding="utf-8") as handle:
|
|
handle.write(after)
|
|
os.replace(temporary, path)
|
|
except OSError as error:
|
|
print(f"classroom-desktop-layout: {error}", file=sys.stderr)
|
|
return 1
|
|
|
|
print(f"classroom-desktop-layout: DVD launcher "
|
|
f"{'added' if wanted else 'removed'}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|