fix: make an existing classroom converge into the Plasma appliance

A real room upgraded to 0.1.45 and came up as stock Debian Plasma with
the old Waybar across the top, mako still catching notifications, and a
lock screen on an account that has no password.

The cause was one line, and it was in the lab: the harness handed the
session under test XDG_CONFIG_DIRS by hand, so every result it had ever
produced was a result about a machine somebody had already configured.
On a machine SDDM starts the session through PAM, PAM does not read
/etc/environment.d, and Debian's own plasma-workspace env.sh then set the
variable from scratch — so the classroom's settings directory was not in
the cascade at all. No layout, no lock policy, no Kiosk restrictions.

The settings now arrive the way Plasma delivers them, through a file in
/etc/xdg/plasma-workspace/env. Around that: the look and feel is
immutable on its own key so a stale profile cannot answer for it; the
palette is composed into the package, because naming a colour scheme does
not produce one and the dark bar had been coming from a technician's
hand; the desktop reset converges another desktop's appearance state and
keeps the room's own; and classroom-migrate stops, deactivates and masks
the old shell, including the D-Bus activation that brought mako back
while the panel was down — found only by killing the panel on a migrated
machine.

device.conf loses COMPOSITOR and comes off the hyprland channel, which
would otherwise have handed the room the experiment back.

The lab stops flattering the package: it restarts the machine, lets SDDM
actually autologin, and asks a fresh install and a machine upgraded from
the Hyprland package the same sixty questions. Both answer identically,
twice, and the panel still comes back from eight SIGKILLs.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
ekklesia
2026-09-10 23:06:56 -05:00
co-authored by Claude Opus 5
parent 6050144e5d
commit 926668dc9b
29 changed files with 1588 additions and 40 deletions
+22
View File
@@ -0,0 +1,22 @@
[D-BUS Service]
# The old notification daemon, made unstartable.
#
# Removing mako's activation from systemd was not enough, and this was
# measured rather than guessed: during the crash-recovery test on a
# migrated machine, the panel was killed, something in the session sent a
# notification while nothing owned org.freedesktop.Notifications, and
# D-Bus started mako from the file mako-notifier ships. The room would
# then have had the old shell's popups on a Plasma desktop, from a daemon
# with no configuration, until the next reboot.
#
# This file shadows that one. The session bus searches XDG_DATA_DIRS in
# order and /usr/local/share comes before /usr/share, which is the same
# mechanism the classroom already uses to shadow a desktop entry — no
# Debian file is touched, and mako-notifier can be upgraded or removed
# without any of this needing to change.
#
# /bin/false is what a machine with no notification daemon installed does
# anyway: the activation fails and the caller is told so. Plasma's own
# service takes the name at startup and never comes through here.
Name=org.freedesktop.Notifications
Exec=/bin/false
+33
View File
@@ -0,0 +1,33 @@
# Put the classroom's settings into the session's configuration cascade.
#
# This is the file the migration failure turned on. Everything the room is
# — the panel layout, the lock screen that must never appear, the idle
# behaviour, the Kiosk restrictions — lives in /etc/ekklesia-classroom/xdg
# and reaches Plasma only if that directory is in XDG_CONFIG_DIRS. It was
# set in /etc/environment.d, which systemd's user manager reads. Measured
# on a real machine and then reproduced in the lab: that is not enough.
#
# startplasma sources /etc/xdg/plasma-workspace/env/*.sh at session start,
# and Debian's env.sh there sets XDG_CONFIG_DIRS from scratch when the
# variable is empty — which it is, because SDDM starts the session through
# PAM, and PAM does not read /etc/environment.d. startplasma then pushes
# its own environment into the user manager, so the value from
# /etc/environment.d was not merely bypassed, it was overwritten for every
# user service as well. The room came up as stock Debian Plasma: kickoff,
# a pager, a system tray, the Debian wallpaper, and a lock screen.
#
# So the classroom sets it where Plasma itself looks. The name sorts after
# env.sh, so this appends to Debian's value rather than replacing it, and
# the whole thing is a no-op if the directory is already there.
#
# The /etc/environment.d file stays: it covers user services that start
# before, or without, a Plasma session.
case ":${XDG_CONFIG_DIRS:-}:" in
*:/etc/ekklesia-classroom/xdg:*)
;;
*)
XDG_CONFIG_DIRS="/etc/ekklesia-classroom/xdg:${XDG_CONFIG_DIRS:-/etc/xdg}"
export XDG_CONFIG_DIRS
;;
esac
+12 -1
View File
@@ -48,7 +48,18 @@ XftSubPixel=rgb
[KDE] [KDE]
widgetStyle=Breeze widgetStyle=Breeze
LookAndFeelPackage=org.ekklesia.classroom.desktop # One key, marked immutable on its own rather than by freezing the group.
#
# This is the name plasmashell reads to decide which layout script builds a
# panel for a session that has none, so it decides whether the room comes
# up as the classroom or as stock Plasma. A migrated machine reached this
# experiment with a user profile that had been through other desktops, and
# a user's own kdeglobals sits ahead of every system directory in the
# cascade. Immutable here means no leftover profile, and no stray write by
# a settings dialog that does not exist in this room anyway, can answer
# that question differently. The palette below stays writable, because
# classroom-theme is allowed to change it.
LookAndFeelPackage[$i]=org.ekklesia.classroom.desktop
SingleClick=false SingleClick=false
AnimationDurationFactor=0.5 AnimationDurationFactor=0.5
+27 -5
View File
@@ -1,25 +1,47 @@
# The desktop half of "the power button does nothing". # The classroom never sleeps, never dims, and never locks.
# #
# PowerDevil takes logind's handle-power-key inhibitor while a session is # Two failures on real hardware are behind this file. The power button put
# running, so the logind drop-in on its own would be overruled by # a room to sleep; and a machine left alone came back to a password prompt
# whatever this says. 0 is no action. # on an account that is meant to have no password, which strands the
# television until somebody arrives with SSH.
# #
# Immutable, so nothing in the session can put an action back. # PowerDevil decides all of it. It also takes logind's handle-power-key
# inhibitor while a session is running, so the logind drop-in on its own
# would be overruled by whatever this says.
#
# Every profile is marked immutable at the profile level, not just on the
# subgroups. PowerDevil loads an idle action only when the profile has a
# group for it, so what matters here is as much what is absent — no
# DimDisplay, no DPMSControl, no SuspendSession — as what is present, and
# a writable profile is one a leftover user file can add those back to.
# classroom-desktop-reset removes the user's copy at every login for the
# same reason.
# #
# Every profile gets the same answer. A classroom machine has no battery, # Every profile gets the same answer. A classroom machine has no battery,
# but a profile that exists and disagrees is worse than one that does not # but a profile that exists and disagrees is worse than one that does not
# exist at all. # exist at all.
#
# 0 is no action.
[AC][$i]
icon=battery-charging
[AC][HandleButtonEvents][$i] [AC][HandleButtonEvents][$i]
powerButtonAction=0 powerButtonAction=0
lidAction=0 lidAction=0
triggerLidActionWhenExternalMonitorPresent=false triggerLidActionWhenExternalMonitorPresent=false
[Battery][$i]
icon=battery-060
[Battery][HandleButtonEvents][$i] [Battery][HandleButtonEvents][$i]
powerButtonAction=0 powerButtonAction=0
lidAction=0 lidAction=0
triggerLidActionWhenExternalMonitorPresent=false triggerLidActionWhenExternalMonitorPresent=false
[LowBattery][$i]
icon=battery-low
[LowBattery][HandleButtonEvents][$i] [LowBattery][HandleButtonEvents][$i]
powerButtonAction=0 powerButtonAction=0
lidAction=0 lidAction=0
+6 -1
View File
@@ -19,6 +19,11 @@ DisplayServer=wayland
InputMethod= InputMethod=
[Users] [Users]
MaximumUid=1000 # Wide enough to include a classroom account that was created after
# another user. The first migrated room had uid 1001 and would not have
# appeared in the greeter at all; autologin looks the name up directly and
# still worked, but a room that ever falls back to the greeter must show
# the account it is meant to log in as.
MinimumUid=1000 MinimumUid=1000
MaximumUid=1999
HideShells=/usr/sbin/nologin,/bin/false HideShells=/usr/sbin/nologin,/bin/false
@@ -32,3 +32,9 @@ HandleRebootKeyLongPress=ignore
HandleLidSwitch=ignore HandleLidSwitch=ignore
HandleLidSwitchExternalPower=ignore HandleLidSwitchExternalPower=ignore
HandleLidSwitchDocked=ignore HandleLidSwitchDocked=ignore
# logind's own idle timer, which is separate from PowerDevil's and runs
# whether or not a session is up. ignore is the default, and a classroom
# is not a machine to leave a default of this importance implicit on.
IdleAction=ignore
IdleActionSec=0
+199 -11
View File
@@ -937,6 +937,165 @@ ekklesia-classroom-tv.service small, supervised, ours
adapters arrive today; the commands get measured before anything is adapters arrive today; the commands get measured before anything is
built on them. built on them.
## The upgrade that failed
The clean-install result was real and the migration was not. A classroom
machine that had been through the sway build and then the Hyprland
experiment took the 0.1.45 package and came up as **stock Debian Plasma**:
kickoff, a pager, a full system tray, the Debian wallpaper, the old Waybar
still across the top, mako still catching notifications — and then a lock
screen, on an account that has no password. Somebody had to give the
appliance account a password over SSH to get back in.
None of that was visible here, because the lab had been answering an
easier question than the one a room asks.
### Reproducing it
One line of the harness:
```
tools/lab/lab plasma-up: -e XDG_CONFIG_DIRS=/etc/ekklesia-classroom/xdg:/etc/xdg
```
The lab had been handing the session under test the very setting whose
delivery was the thing in doubt. Removing it and starting the session the
way a machine does reproduced the failure exactly: 23 applets, kickoff,
pager, system tray, Debian wallpaper — `docs/screenshots/migration-before.png`
is that screen, and it is the teacher's screen.
### Root causes
**1. The classroom's settings never reached the session.** Everything the
room is — the panel layout, the lock screen that must never appear, the
idle behaviour, every Kiosk restriction — lives in
`/etc/ekklesia-classroom/xdg`, and reaches Plasma only if that directory
is in `XDG_CONFIG_DIRS`. It was set in `/etc/environment.d`, which
systemd's **user manager** reads. SDDM starts the session through PAM, and
PAM does not read `/etc/environment.d`. Debian's own
`/etc/xdg/plasma-workspace/env/env.sh` then set the variable from scratch,
because it was empty, and `startplasma` pushed its own environment back
into the user manager — so the value was not merely bypassed, it was
overwritten for every user service too. Measured in the session that came
up:
```
XDG_CONFIG_DIRS=/home/classroom/.config/kdedefaults:/etc/xdg:/usr/share/desktop-base/kf5-settings
LookAndFeelPackage = org.debian.desktop
Autolock = (unset, so on)
shell_access = (unset, so allowed)
```
The stock panel, the lock screen and the absence of every Kiosk
restriction are all that one line.
**2. Deleting the panel configuration made it worse.** The desktop reset
removes `plasma-org.kde.plasma.desktop-appletsrc` before the shell starts
so that the bar always comes from the package. With the look and feel
unresolvable, Plasma rebuilt the **Debian** default layout instead. The
reset was correct and was pointed at the wrong answer.
**3. Naming a colour scheme does not produce one.** `ColorScheme=EkklesiaDark`
is a label; KConfig reads the actual colours from `[Colors:*]` groups in
kdeglobals, and nothing was putting them there except a technician having
run `classroom-theme` once by hand. The clean-install screenshots looked
right for a reason the package did not own. A machine with no user
profile got Breeze light — confirmed here the moment a profile was
cleared.
**4. A stale user profile outranks the package.** `~/.config/kdeglobals`
sits ahead of every system directory, and `~/.config/kdedefaults` ahead of
ours. A migrated machine carried a Breeze-light palette in both.
**5. The old shell was still being started.** `waybar.service` was
activated through `graphical-session.target.wants` — and, found only by
running the crash-recovery suite on a migrated machine, **mako came back
through D-Bus**: the panel was killed, something sent a notification while
nothing owned `org.freedesktop.Notifications`, and D-Bus started mako from
the file `mako-notifier` ships. Deactivating the systemd unit would not
have been enough.
And one more that had not bitten yet: `device.conf` still said
`CHANNEL=hyprland`, so the next release published on that channel would
have handed the room the Hyprland experiment back.
### The fix
| | |
| --- | --- |
| `config/plasma/env/zz-ekklesia-classroom.sh` | new. `/etc/xdg/plasma-workspace/env/` is sourced by `startplasma` for every session; the name sorts after Debian's `env.sh`, so it appends rather than replaces, and it is a no-op if the directory is already there. This is the root fix. |
| `config/plasma/xdg/kdeglobals` | `LookAndFeelPackage[$i]` — per-entry immutability, so no leftover profile and no stray write can answer that question differently. The palette stays writable, because `classroom-theme` may change it. |
| `tools/build-kdeglobals` | new. Composes the shipped kdeglobals from the session policy **and** `EkklesiaDark.colors`, so the room is dark because the package says so. One source of truth: it composes, it does not duplicate. |
| `scripts/classroom-desktop-reset` | asserts the cascade and repairs it for the shell about to start if it is wrong; removes another desktop's look and feel defaults and a foreign palette; keeps an Ekklesia one; clears a user power profile. |
| `scripts/classroom-migrate` | new. Stops, deactivates and masks the old shell's units, removes activation from all three directories it can live in, removes hand-written unit files that no Debian package owns, clears old autostart entries, removes the login shell that used to start sway, and migrates `device.conf`. Run from `postinst` on every install. |
| `config/dbus/fr.emersion.mako.service` | new. Shadows mako's D-Bus activation from `/usr/local/share`, where the session bus looks first. `Exec=/bin/false`, which is what a machine with no notification daemon does anyway. |
| `config/plasma/xdg/powermanagementprofilesrc` | profiles immutable at the profile level, not just on the button subgroups: PowerDevil loads an idle action when it finds a group for it, so a writable profile is one a leftover user file can add a suspend to. |
| `config/systemd/logind-ekklesia-classroom.conf` | `IdleAction=ignore` as well. |
| `config/sddm/ekklesia-classroom.conf` | uid range 1000–1999. The first migrated room's classroom account was uid 1001 and would not have appeared in the greeter at all. |
### What device.conf means now
`COMPOSITOR` and `HYPR_APPEARANCE` are removed on upgrade. There is one
desktop stack and nothing left for them to choose; a stale value is an
instruction this package cannot carry out.
`CHANNEL=hyprland` becomes `CHANNEL=classroom-v2`. Not `stable` and not
`canary`: both are the sway line, and a release published there would
convert a v2 machine back. **That channel is not published yet**, so a
migrated machine takes no updates until it is — which is the honest state
of an unpublished experiment, and better than being quietly reverted.
Everything else in the file, the broker password included, is rewritten
line by line and byte-for-byte.
### The tests that would have caught it
- `tools/lab/lab plasma-up` no longer sets `XDG_CONFIG_DIRS`. The package
has to put it there, the way it has to on a machine.
- `tools/lab/lab reboot` — a real restart of the machine under test: new
PID 1, new user manager, every drop-in and generator read again. **SDDM
autologin genuinely runs here**: it authenticates the classroom through
PAM and launches the Plasma session, and then has no display hardware to
draw on. Both scenarios now go through it.
- `tools/lab/converged-check` — 60 properties of a converged machine,
asked of the running system and never of the repository. Both scenarios
run the same list, because a fresh install and a migrated one have to be
indistinguishable from a teacher's chair.
- `tools/lab/dirty-upgrade` — the Hyprland package actually installed,
with sway, waybar and mako behind it; Waybar activated in all three
directories; an autostart entry; a device.conf asking for a compositor
that is gone; a Breeze-light profile in both places that outrank the
package; a stock panel already built; and a user power profile that
would suspend the room. Then the new package, a reboot, and the
questions.
- `tests/test-migration.sh` — 41 source-level checks, one per reason the
failure happened.
### Results
| | |
| --- | --- |
| dirty upgrade → converged | 60 / 60 |
| installed a second time over itself | 60 / 60, device.conf unchanged |
| clean install → converged | 60 / 60 |
| panel killed eight times | back every time, 2.2–7.4s |
| compositor killed twice | 0.1–0.4s |
| audio policy killed three times | active every time |
| the room's light theme across a logout | survived |
| every test script | 11 / 11 |
`docs/screenshots/migration-after.png` and `v2-from-package.png` are the
migrated machine and the fresh one. They are the same desktop.
### One regression this pass made and caught
The first version of the reset deleted `~/.config/kdedefaults`
unconditionally. Plasma treats that directory as the record of which look
and feel is in force, so deleting it made Plasma re-apply the look and
feel at every login — which throws away the room's chosen palette.
Measured: `classroom-theme light`, log out, and the bar came back dark.
The rule is now whose the defaults are: the classroom's are kept, another
desktop's are removed.
## Unresolved risks ## Unresolved risks
In the order I would worry about them. In the order I would worry about them.
@@ -960,10 +1119,13 @@ In the order I would worry about them.
tries disturbs the session enough to close the card. The name was tries disturbs the session enough to close the card. The name was
typed through a lab-only `--child` argument. **Type a name on the typed through a lab-only `--child` argument. **Type a name on the
bench before a Sunday.** bench before a Sunday.**
4. **SDDM is configured and untested.** It cannot be exercised in a 4. **SDDM has never put a picture on a screen.** More is known than
container, and it is the thing that starts the session. If the config is before: in the lab it really does autologin, authenticating the
wrong the room boots to nothing. This is the first thing to check on a classroom through PAM and launching the Plasma session — that is now
VM or a bench machine, and it is why the checklist below starts there. one of the checks. What it cannot do in a container is find a display
to draw on, so everything from the compositor's first frame onwards is
still a bench test. It remains the first thing to check on a VM or a
bench machine.
5. **No real audio hardware has been through the policy.** The decision is 5. **No real audio hardware has been through the policy.** The decision is
well tested; applying it is not. A wrong `pactl set-card-profile` on well tested; applying it is not. A wrong `pactl set-card-profile` on
real hardware is a silent room. real hardware is a silent room.
@@ -988,7 +1150,17 @@ In the order I would worry about them.
registered from the packaged entry and its action opens a shell — registered from the packaged entry and its action opens a shell —
verified over D-Bus — but key injection cannot reach the nested verified over D-Bus — but key injection cannot reach the nested
session, so `Meta+Ctrl+Alt+Shift+K` itself is unpressed. session, so `Meta+Ctrl+Alt+Shift+K` itself is unpressed.
13. **Ctrl+Alt+T is still registered.** Konsole's own entry declares it 13. **Idle behaviour is configured and unobserved.** No suspend, no dim,
no blank and no lock are all now in the package, immutable, and read
back correctly from a migrated session. Nothing in a container goes
idle, so none of it has been watched not happening. **Leave a bench
machine alone for an hour.**
14. **The v2 channel does not exist yet.** A migrated machine is moved to
`CHANNEL=classroom-v2` so that a stable or Hyprland release cannot
take it back, and nothing is published there. Until it is, a v2
machine takes no updates at all. That is deliberate; it is also a
thing to remember before leaving one in a room.
15. **Ctrl+Alt+T is still registered.** Konsole's own entry declares it
and the registration survives being shadowed. It is inert, because and the registration survives being shadowed. It is inert, because
Konsole refuses to open a shell under the classroom's Konsole refuses to open a shell under the classroom's
`shell_access=false` — measured — but it rests on that one `shell_access=false` — measured — but it rests on that one
@@ -1078,6 +1250,16 @@ The existing Debian classroom test VM should be kept.
package, hidden from every application list, granting no privilege. package, hidden from every application list, granting no privilege.
* **Long-Escape investigated and recommended against**, with the two * **Long-Escape investigated and recommended against**, with the two
clean alternatives written down. clean alternatives written down.
* **The migration failure found on a real room and fixed in the package**:
the settings directory now reaches the session the way Plasma delivers
it, the look and feel cannot be answered for by a stale profile, the
palette is in the package rather than in somebody's home directory, and
the old shell is stopped, deactivated, masked and — the one that was
only found by killing the panel — unable to come back through D-Bus.
* **The lab stopped flattering the package**: it no longer sets the
session's configuration path by hand, it restarts the machine, and it
asks a fresh install and a machine upgraded from the Hyprland
experiment the same sixty questions. Both answer them identically.
**Next**, in order: **Next**, in order:
@@ -1090,11 +1272,17 @@ The existing Debian classroom test VM should be kept.
disc. These three are the only things left that a container cannot disc. These three are the only things left that a container cannot
answer, and each of them is a Sunday-morning path. Add the power answer, and each of them is a Sunday-morning path. Add the power
button and the maintenance chord to that list. button and the maintenance chord to that list.
2. Publish `0.1.45~plasma1` to a channel of its own, the way the Hyprland 2. Publish the v2 channel. `classroom-migrate` already moves a migrated
milestone did, so a stable or canary room can never be offered it. room onto `CHANNEL=classroom-v2`, and nothing serves that manifest
3. Bench-test SDDM: autologin, and that ending the session brings the yet, so those machines are taking no updates at all. This is now the
classroom back rather than a shell. first packaging job rather than a tidy-up.
4. When the CEC adapters have been exercised, replace the computer volume 3. Take the upgraded VM through the same list by hand:
`tools/lab/converged-check` is the shape of it, and the things a
container cannot answer are the lock screen never appearing after an
hour alone, and the machine still being there on Monday.
4. Bench-test SDDM on hardware: autologin all the way to a picture, and
that ending the session brings the classroom back rather than a shell.
5. When the CEC adapters have been exercised, replace the computer volume
applet with a television control, behind the service boundary in applet with a television control, behind the service boundary in
*Television control, and where CEC will fit*. *Television control, and where CEC will fit*.
5. Live View, when the rest is quiet. 6. Live View, when the rest is quiet.
+10
View File
@@ -77,6 +77,16 @@ container lab. See [../classroom-v2-experiment.md](../classroom-v2-experiment.md
| `lock-panel-rmb` | right-clicking the bar: no menu | | `lock-panel-rmb` | right-clicking the bar: no menu |
| `lock-krunner` | KRunner asked for, and not there | | `lock-krunner` | KRunner asked for, and not there |
## The migration
| | |
| --- | --- |
| `migration-before` | what a real room got when it upgraded: stock Debian Plasma, kickoff, a pager, a system tray, the Debian wallpaper. Reproduced in the lab by taking one line out of the harness. |
| `migration-after` | the same machine after the fix — a Hyprland-era install, upgraded, rebooted |
| `migration-after-bar` | its bar, at the size a teacher sees it |
| `migration-parent-call` | Parent Call on the migrated machine, saying it cannot reach the church network rather than pretending it can |
| `theme-light-live`, `theme-light-after-logout` | the room's own light theme, and the same room after logging out and back in. The first attempt at this pass lost it. |
## Failures, kept ## Failures, kept
| | | | | |
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 253 KiB

+7 -7
View File
@@ -8,13 +8,6 @@ if ! id classroom >/dev/null 2>&1; then
classroom classroom
fi fi
# The session is started by SDDM now. An upgrade from a sway-era room has
# to remove the login shell that used to start it, or the machine has two
# things trying to own tty1.
rm -f /home/classroom/.bash_profile
rm -f /etc/systemd/system/[email protected]/autologin.conf
rmdir --ignore-fail-on-non-empty \
/etc/systemd/system/[email protected] 2>/dev/null || true
# Commands the room's own parts install. Listing them by name went wrong # Commands the room's own parts install. Listing them by name went wrong
# once already, when a command that had been removed was still chmod'ed # once already, when a command that had been removed was still chmod'ed
@@ -44,6 +37,13 @@ PY
chown root:classroom /etc/ekklesia-classroom/device.conf 2>/dev/null || true chown root:classroom /etc/ekklesia-classroom/device.conf 2>/dev/null || true
chmod 0640 /etc/ekklesia-classroom/device.conf chmod 0640 /etc/ekklesia-classroom/device.conf
# Converge a machine that was something else: the old bar and notification
# daemon stopped and deactivated, the login shell that used to start sway
# removed, and device.conf moved off settings this package cannot carry
# out. Runs on every install, does nothing on a machine that is already
# this classroom.
/usr/local/bin/classroom-migrate || true
systemctl daemon-reload || true systemctl daemon-reload || true
# The power button stops working immediately rather than at the next # The power button stops working immediately rather than at the next
+120 -5
View File
@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
set -euo pipefail set -euo pipefail
# Make the panel come from the package, every time. # Make the desktop come from the package, every time.
# #
# Plasma builds the panel from the classroom's layout script whenever it # Plasma builds the panel from the classroom's layout script whenever it
# starts with no panel configuration, and keeps whatever it built after # starts with no panel configuration, and keeps whatever it built after
@@ -17,16 +17,131 @@ set -euo pipefail
# #
# Nothing is lost by this. The panel is immutable to teachers by Kiosk # Nothing is lost by this. The panel is immutable to teachers by Kiosk
# policy, so there is never anything of theirs in that file. # policy, so there is never anything of theirs in that file.
#
# This runs before plasmashell, as a user service ordered Before it.
CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/plasma-org.kde.plasma.desktop-appletsrc" CLASSROOM_XDG=/etc/ekklesia-classroom/xdg
CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
say() {
echo "classroom-desktop-reset: $1"
}
# THE CASCADE.
#
# Everything below this line is pointless if Plasma is not reading the
# classroom's settings directory at all: without it the room gets stock
# Plasma, no Kiosk restrictions and a lock screen. The session environment
# is set by /etc/xdg/plasma-workspace/env/zz-ekklesia-classroom.sh, which
# is the mechanism Plasma itself provides. This is the assertion, not the
# mechanism: if the variable arrives here wrong, say so out loud and fix it
# for the shell that is about to start rather than build a wrong desktop
# quietly.
case ":${XDG_CONFIG_DIRS:-}:" in
*:"$CLASSROOM_XDG":*)
;;
*)
say "XDG_CONFIG_DIRS did not contain $CLASSROOM_XDG — repairing it"
export XDG_CONFIG_DIRS="$CLASSROOM_XDG:${XDG_CONFIG_DIRS:-/etc/xdg}"
systemctl --user set-environment \
"XDG_CONFIG_DIRS=$XDG_CONFIG_DIRS" 2>/dev/null || true
;;
esac
# FOREIGN APPEARANCE STATE.
#
# A classroom machine that has been through another desktop arrives with a
# profile that answers questions the package means to answer. Two places
# sit ahead of the classroom's own directory in KConfig's cascade:
#
# ~/.config/kdedefaults what the last look and feel applied wrote.
# Plasma also treats it as the record of which
# look and feel is in force: remove it and the
# next session applies ours again from scratch,
# which resets the palette to the package
# default. That is right for a machine arriving
# from another desktop and wrong for a room that
# chose the light theme, so whose it is decides.
#
# ~/.config/kdeglobals the user's own file, which beats every system
# directory. plasma-apply-colorscheme expands a
# whole palette into it, so a light Breeze scheme
# from a previous desktop would keep the room
# light no matter what the package said.
#
# The colour scheme is the one piece of appearance a room is allowed to
# choose, through classroom-theme, so an Ekklesia scheme is kept and
# anything else is removed. This is not a wipe of the home directory: two
# named pieces of appearance state, neither of them a teacher's work.
# The [General] ColorScheme of a KConfig file, or nothing.
scheme_of() {
awk -F= '
/^\[/ { general = ($0 == "[General]"); next }
general && $1 == "ColorScheme" { print $2; exit }
' "$1" 2>/dev/null || true
}
defaults_are_ours=no
if [[ -d "$CONFIG_HOME/kdedefaults" ]]; then
case "$(scheme_of "$CONFIG_HOME/kdedefaults/kdeglobals")" in
Ekklesia*)
defaults_are_ours=yes
;;
*)
rm -rf "$CONFIG_HOME/kdedefaults"
say "removed another desktop's look and feel defaults"
;;
esac
fi
if [[ -f "$CONFIG_HOME/kdeglobals" ]]; then
scheme=$(scheme_of "$CONFIG_HOME/kdeglobals")
case "$scheme" in
Ekklesia*)
say "keeping the room's colour scheme ($scheme)"
;;
"")
# No scheme named. That is what Plasma leaves behind after it
# applies a look and feel — it writes a hash instead — so the
# palette belongs to whichever look and feel last ran, and the
# directory above has just said whose that was.
if [[ "$defaults_are_ours" == yes ]]; then
say "keeping the palette the classroom's look and feel left"
else
rm -f "$CONFIG_HOME/kdeglobals"
say "removed an unnamed palette from another desktop"
fi
;;
*)
rm -f "$CONFIG_HOME/kdeglobals"
say "removed a foreign palette ($scheme); the package supplies one"
;;
esac
fi
# The room's power behaviour is policy, not preference: never sleep, never
# dim, never lock. PowerDevil loads an idle action when it finds a group
# for it, and the merged view of this file is where such a group could
# come from, so the user's copy goes at every login. Nothing of a
# teacher's is in it — there is no settings application in this room.
if [[ -f "$CONFIG_HOME/powermanagementprofilesrc" ]]; then
rm -f "$CONFIG_HOME/powermanagementprofilesrc"
say "cleared the user's power settings; the package supplies them"
fi
CONFIG="$CONFIG_HOME/plasma-org.kde.plasma.desktop-appletsrc"
if [[ -e "$CONFIG" ]]; then if [[ -e "$CONFIG" ]]; then
rm -f "$CONFIG" rm -f "$CONFIG"
echo "classroom-desktop-reset: panel will be rebuilt from the layout" say "panel will be rebuilt from the layout"
else else
echo "classroom-desktop-reset: no panel configuration to clear" say "no panel configuration to clear"
fi fi
# The icon cache remembers a name it once failed to find, which outlives # The icon cache remembers a name it once failed to find, which outlives
# the package that later supplies it. # the package that later supplies it.
rm -f "${XDG_CACHE_HOME:-$HOME/.cache}/icon-cache.kcache" 2>/dev/null || true rm -f "$CACHE_HOME/icon-cache.kcache" 2>/dev/null || true
+225
View File
@@ -0,0 +1,225 @@
#!/bin/bash
set -uo pipefail
# Turn a machine that was something else into this classroom.
#
# A room does not arrive new. It arrives as whatever it was before: the
# sway classroom, the Hyprland experiment, a machine a technician tried
# something on. The package can describe the desktop it wants perfectly
# and still lose to one leftover symlink, which is what happened: a real
# room upgraded to the Plasma appliance and came up with the old Waybar
# across the top, mako still catching notifications, and Plasma's own
# stock panel underneath.
#
# So the installation converges the machine instead of assuming it.
# Everything here is something a classroom package created, or state this
# package can identify as belonging to a classroom shell it replaces.
# Nothing removes a file Debian owns: the old bar and the old notification
# daemon stay installed and simply stop being started.
#
# Idempotent, and safe on a machine that has none of this. Run by hand at
# any time:
#
# sudo classroom-migrate
CLASSROOM_USER=classroom
DEVICE_CONFIG=/etc/ekklesia-classroom/device.conf
# The shells this appliance replaces. There is exactly one desktop stack
# now — SDDM, KWin, plasmashell — and every one of these is a component of
# a stack that is gone.
LEGACY_UNITS=(waybar.service mako.service swayidle.service swaybg.service)
LEGACY_PROCESSES=(waybar mako classroom-dock swaybg swayidle)
LEGACY_EXEC_PATTERN='waybar|mako|classroom-dock|classroom-notifications|sway|Hyprland|start-hyprland|classroom-session'
say() {
echo "[classroom-migrate] $1"
}
if [[ "$(id -u)" -ne 0 ]]; then
say "must be run as root"
exit 1
fi
home=$(getent passwd "$CLASSROOM_USER" | cut -d: -f6)
uid=$(id -u "$CLASSROOM_USER" 2>/dev/null || true)
# ---------------------------------------------------------------- units
# Stop what is running now. The upgrade happens underneath a live session,
# so the old bar is on the screen while this runs.
if [[ -n "$uid" && -d "/run/user/$uid" ]]; then
for unit in "${LEGACY_UNITS[@]}"; do
runuser -u "$CLASSROOM_USER" -- env "XDG_RUNTIME_DIR=/run/user/$uid" \
systemctl --user stop "$unit" >/dev/null 2>&1 || true
done
fi
for process in "${LEGACY_PROCESSES[@]}"; do
if pkill -u "$CLASSROOM_USER" -x "$process" 2>/dev/null; then
say "stopped $process"
fi
done
# Remove the activation, wherever it was made. `systemctl --global enable`
# writes into /etc/systemd/user; a session that set XDG_CONFIG_DIRS gets
# /etc/xdg/systemd/user as well; and a unit enabled by hand as the
# classroom user lands in the home directory. The real room had it in the
# second of those, which is why guessing one location would not have been
# enough.
wants_roots=(/etc/systemd/user /etc/xdg/systemd/user /run/systemd/user)
[[ -n "$home" ]] && wants_roots+=("$home/.config/systemd/user")
for root in "${wants_roots[@]}"; do
[[ -d "$root" ]] || continue
for unit in "${LEGACY_UNITS[@]}"; do
while IFS= read -r link; do
rm -f "$link"
say "removed activation ${link#/}"
done < <(find "$root" -name "$unit" -path '*.wants/*' 2>/dev/null)
done
done
# The unit files themselves, where somebody wrote them by hand. Neither
# waybar nor mako ships a systemd unit in trixie, so a file by one of
# those names belongs to the shell this package replaces — but the rule is
# checked rather than assumed: anything a Debian package owns is left
# exactly where it is.
for root in /etc/systemd/user /etc/xdg/systemd/user "${home:+$home/.config/systemd/user}"; do
[[ -n "$root" && -d "$root" ]] || continue
for unit in "${LEGACY_UNITS[@]}"; do
[[ -f "$root/$unit" ]] || continue
if dpkg -S "$root/$unit" >/dev/null 2>&1; then
say "left $root/$unit alone: a Debian package owns it"
continue
fi
rm -f "$root/$unit"
say "removed the hand-written $root/$unit"
done
done
# And then say no once more, at the system level, so that a unit file
# reappearing from anywhere cannot start it. A mask is a symlink to
# /dev/null in /etc/systemd/user: visible, reversible, and it removes
# nothing.
systemctl --global mask "${LEGACY_UNITS[@]}" >/dev/null 2>&1 || true
# And the other way the old notification daemon starts: D-Bus activation.
# The package shadows mako's service file so that activation fails, but a
# session bus that is already running read its service directories at
# startup and has not seen it. Measured during crash recovery on a
# migrated machine: the panel was killed, a notification was sent while
# nothing owned the name, and mako took it.
if [[ -n "$uid" && -S "/run/user/$uid/bus" ]]; then
runuser -u "$CLASSROOM_USER" -- env \
"XDG_RUNTIME_DIR=/run/user/$uid" \
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$uid/bus" \
dbus-send --session --dest=org.freedesktop.DBus --type=method_call \
/org/freedesktop/DBus org.freedesktop.DBus.ReloadConfig \
>/dev/null 2>&1 || true
fi
# ------------------------------------------------------------- autostart
# XDG autostart entries that start a component of the old shell. Matched on
# what they run, not on their filename, and only in the classroom's own
# directory.
if [[ -n "$home" && -d "$home/.config/autostart" ]]; then
for entry in "$home/.config/autostart"/*.desktop; do
[[ -e "$entry" ]] || continue
if grep -qE "^Exec=.*($LEGACY_EXEC_PATTERN)" "$entry" 2>/dev/null; then
rm -f "$entry"
say "removed autostart entry $(basename "$entry")"
fi
done
fi
# ------------------------------------------------------------ tty1 login
# The sway classroom started its session from the login shell of an
# autologin getty. SDDM owns the session now, and two things owning tty1
# is how a machine ends up with no usable screen at all.
if [[ -n "$home" && -f "$home/.bash_profile" ]]; then
if grep -qE "exec .*($LEGACY_EXEC_PATTERN)" "$home/.bash_profile" 2>/dev/null; then
rm -f "$home/.bash_profile"
say "removed the login shell that used to start the old session"
fi
fi
rm -f /etc/systemd/system/[email protected]/autologin.conf
rmdir --ignore-fail-on-non-empty \
/etc/systemd/system/[email protected] 2>/dev/null || true
# --------------------------------------------------------- device.conf
# The room's own settings file outlives every version of this package, so
# it is edited rather than replaced: the broker password is in it.
#
# COMPOSITOR chose between sway and Hyprland. There is nothing left for it
# to choose, and a stale value is worse than none — it is an instruction
# this package cannot carry out.
#
# CHANNEL decides which manifest the updater fetches. A machine left on the
# hyprland channel would be handed the Hyprland experiment again by the
# next release published there, undoing the upgrade it just took.
if [[ -f "$DEVICE_CONFIG" ]]; then
python3 - "$DEVICE_CONFIG" <<'PY'
import re
import sys
OBSOLETE_KEYS = ("COMPOSITOR", "HYPR_APPEARANCE")
# Channels that served a desktop this package replaces.
OBSOLETE_CHANNELS = {"hyprland"}
CHANNEL = "classroom-v2"
path = sys.argv[1]
with open(path, "r", encoding="utf-8") as handle:
lines = handle.readlines()
out = []
changed = []
for line in lines:
key = line.split("=", 1)[0].strip() if "=" in line else ""
if key in OBSOLETE_KEYS:
changed.append(f"dropped {key}")
continue
if key == "CHANNEL":
value = line.split("=", 1)[1].strip()
if value.lower() in OBSOLETE_CHANNELS:
changed.append(f"moved off the {value} channel onto {CHANNEL}")
line = f"CHANNEL={CHANNEL}\n"
out.append(line)
if changed:
with open(path, "w", encoding="utf-8") as handle:
handle.writelines(out)
for note in changed:
print(f"[classroom-migrate] device.conf: {note}")
PY
fi
# ------------------------------------------------------- the old session
# Plasma reads the classroom's settings out of a directory that has to be
# in XDG_CONFIG_DIRS, and the environment.d file that used to put it there
# never reached a session started by SDDM. The session script that does is
# installed by the package; this only checks it arrived, because a room
# without it is a room with no lockdown and no panel of ours.
if [[ ! -r /etc/xdg/plasma-workspace/env/zz-ekklesia-classroom.sh ]]; then
say "WARNING: the session environment script is missing;"
say " the desktop will come up as stock Plasma"
fi
say "done"
+17 -2
View File
@@ -99,7 +99,15 @@ print("4b2. the session settings do not fight another package")
# live in a directory of their own, in front of /etc/xdg. # live in a directory of their own, in front of /etc/xdg.
check("settings install to the classroom's own directory", check("settings install to the classroom's own directory",
"$STAGE/etc/ekklesia-classroom/xdg/" in build) "$STAGE/etc/ekklesia-classroom/xdg/" in build)
check("nothing is installed into /etc/xdg", "$STAGE/etc/xdg/" not in build) # One file of ours does live under /etc/xdg, and it is not a settings
# file: it is the session environment drop-in that puts the directory
# above into the cascade at all. A drop-in directory is built to be
# added to; a name plasma-workspace already owns is not.
xdg_paths = re.findall(r'\$STAGE(/etc/xdg/[^"]+)', build)
check("the only file under /etc/xdg is the session env drop-in",
xdg_paths == ["/etc/xdg/plasma-workspace/env/zz-ekklesia-classroom.sh"])
check("no settings file takes a name plasma-workspace owns",
not any(path.rsplit("/", 1)[0] == "/etc/xdg" for path in xdg_paths))
check("and that directory is searched first", check("and that directory is searched first",
"XDG_CONFIG_DIRS=/etc/ekklesia-classroom/xdg:/etc/xdg" in "XDG_CONFIG_DIRS=/etc/ekklesia-classroom/xdg:/etc/xdg" in
(root / "config/environment/90-ekklesia-classroom.conf").read_text()) (root / "config/environment/90-ekklesia-classroom.conf").read_text())
@@ -132,8 +140,15 @@ check("PowerDevil: no action on the power button",
power.count("powerButtonAction=0") == 3) power.count("powerButtonAction=0") == 3)
check("PowerDevil: no action on a lid either", check("PowerDevil: no action on a lid either",
power.count("lidAction=0") == 3) power.count("lidAction=0") == 3)
# Six: the three button groups, and the three profiles around them.
# PowerDevil loads an idle action when it finds a group for it, so a
# writable profile is one a leftover user file can add a suspend to.
check("and the session cannot put an action back", check("and the session cannot put an action back",
power.count("[$i]") == 3) power.count("[$i]") == 6)
check("no idle action in any profile",
not any(f"[{profile}][{action}]" in power
for profile in ("AC", "Battery", "LowBattery")
for action in ("SuspendSession", "DimDisplay", "DPMSControl")))
check("the logind drop-in is packaged", check("the logind drop-in is packaged",
"/etc/systemd/logind.conf.d/ekklesia-classroom.conf" in build) "/etc/systemd/logind.conf.d/ekklesia-classroom.conf" in build)
check("the PowerDevil profile is packaged", check("the PowerDevil profile is packaged",
+208
View File
@@ -0,0 +1,208 @@
#!/bin/bash
set -uo pipefail
# The upgrade path, as far as source can be checked without a machine.
#
# A real room upgraded to the Plasma classroom and came up as stock Debian
# Plasma with the old Waybar still across the top and a lock screen waiting
# for a password the account does not have. Every check here is one of the
# reasons that happened. The lab scenario in tools/lab/dirty-upgrade is the
# other half: this one guards the package, that one guards the machine.
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
python3 - "$ROOT" <<'PY'
import sys
from pathlib import Path
root = Path(sys.argv[1])
env_script = (root / "config/plasma/env/zz-ekklesia-classroom.sh").read_text()
build = (root / "tools/build-release.sh").read_text()
postinst = (root / "packaging/debian/postinst").read_text()
migrate = (root / "scripts/classroom-migrate").read_text()
reset = (root / "scripts/classroom-desktop-reset").read_text()
kdeglobals = (root / "config/plasma/xdg/kdeglobals").read_text()
power = (root / "config/plasma/xdg/powermanagementprofilesrc").read_text()
locker = (root / "config/plasma/xdg/kscreenlockerrc").read_text()
logind = (root / "config/systemd/logind-ekklesia-classroom.conf").read_text()
sddm = (root / "config/sddm/ekklesia-classroom.conf").read_text()
lab = (root / "tools/lab/lab").read_text()
passed = failed = 0
def check(label, condition):
global passed, failed
if condition:
print(f" ok {label}")
passed += 1
else:
print(f" FAIL {label}")
failed += 1
print("the session environment")
# The root cause. /etc/environment.d is read by systemd's user manager;
# SDDM starts the session through PAM, which is not systemd, so the
# classroom's settings directory never reached Plasma. startplasma sources
# /etc/xdg/plasma-workspace/env, which is why the file lives there.
check(
"the classroom directory is set where startplasma looks",
"/etc/xdg/plasma-workspace/env/zz-ekklesia-classroom.sh" in build,
)
check(
"and it is in the package, not only in the lab",
'config/plasma/env/zz-ekklesia-classroom.sh' in build,
)
check(
"it prepends rather than replaces",
"XDG_CONFIG_DIRS=\"/etc/ekklesia-classroom/xdg:${XDG_CONFIG_DIRS:-/etc/xdg}\""
in env_script,
)
check(
"and does nothing when the directory is already there",
"*:/etc/ekklesia-classroom/xdg:*" in env_script,
)
check(
"the name sorts after Debian's own env.sh",
Path("config/plasma/env/zz-ekklesia-classroom.sh").name > "env.sh",
)
check(
"environment.d is still shipped for services outside a session",
"/etc/environment.d/90-ekklesia-classroom.conf" in build,
)
check(
"the lab no longer sets the variable for the session under test",
"-e XDG_CONFIG_DIRS=" not in lab,
)
print("\nthe desktop cannot be answered for by a stale profile")
check(
"the look and feel package is immutable on its own key",
"LookAndFeelPackage[$i]=org.ekklesia.classroom.desktop" in kdeglobals,
)
check(
"the palette is composed into the shipped kdeglobals",
"tools/build-kdeglobals" in build,
)
check(
"another desktop's look and feel defaults are removed at login",
"removed another desktop's look and feel defaults" in reset,
)
check(
"and the classroom's own are kept, so a chosen theme survives",
"defaults_are_ours=yes" in reset,
)
check(
"a foreign palette is removed and an Ekklesia one kept",
"Ekklesia*)" in reset and 'rm -f "$CONFIG_HOME/kdeglobals"' in reset,
)
check(
"the panel is still rebuilt from the packaged layout every login",
"plasma-org.kde.plasma.desktop-appletsrc" in reset,
)
check(
"and the reset says so out loud if the cascade is wrong",
"XDG_CONFIG_DIRS did not contain" in reset,
)
print("\nthe old shell converges away")
check("the migration runs on every install", "classroom-migrate" in postinst)
check("it is in the package", "scripts/classroom-migrate" in build)
for unit in ("waybar.service", "mako.service"):
check(f"{unit} is deactivated", unit in migrate)
check(
"activation is removed from all three places it can live",
"/etc/systemd/user" in migrate
and "/etc/xdg/systemd/user" in migrate
and ".config/systemd/user" in migrate,
)
check("and then masked, so a stray unit file cannot start it", "--global mask" in migrate)
check(
"autostart entries are matched on what they run",
"^Exec=" in migrate and "autostart" in migrate,
)
check(
"the login shell that started sway is removed",
".bash_profile" in migrate,
)
check(
"and the tty1 autologin drop-in with it",
"[email protected]/autologin.conf" in migrate,
)
check(
"the old notification daemon cannot be D-Bus activated",
"/usr/local/share/dbus-1/services/fr.emersion.mako.service" in build,
)
check(
"the shadow makes activation fail rather than hang",
"Exec=/bin/false"
in (root / "config/dbus/fr.emersion.mako.service").read_text(),
)
check(
"and a running session bus is told to re-read its services",
"ReloadConfig" in migrate,
)
check(
"nothing removes a Debian package",
"apt-get remove" not in migrate and "apt remove" not in migrate,
)
check(
"device.conf loses the compositor it can no longer choose",
'OBSOLETE_KEYS = ("COMPOSITOR", "HYPR_APPEARANCE")' in migrate,
)
check(
"and comes off a channel that would undo the upgrade",
'OBSOLETE_CHANNELS = {"hyprland"}' in migrate,
)
check(
"the rest of device.conf is rewritten line by line, not regenerated",
"handle.writelines(out)" in migrate,
)
print("\nnothing puts a password prompt in front of a teacher")
check("no automatic lock", "Autolock=false" in locker)
check("no lock on resume", "LockOnResume=false" in locker)
check("the lock settings are immutable", "[Daemon][$i]" in locker)
check(
"no idle suspend, dim or blank in any profile",
not any(
f"[{profile}][{action}]" in power
for profile in ("AC", "Battery", "LowBattery")
for action in ("SuspendSession", "DimDisplay", "DPMSControl")
),
)
check(
"the profiles themselves are immutable, so none can be added back",
all(f"[{profile}][$i]" in power for profile in ("AC", "Battery", "LowBattery")),
)
check(
"a leftover user power profile is cleared at login",
"powermanagementprofilesrc" in reset,
)
check("logind takes no idle action", "IdleAction=ignore" in logind)
check("the power button is still ignored", "HandlePowerKey=ignore" in logind)
check(
"and a long press is still the firmware's business",
"HandlePowerKeyLongPress=ignore" in logind,
)
check(
"the package holds no account credentials",
"passwd" not in postinst and "chpasswd" not in postinst,
)
check(
"a classroom account created after another user still appears",
"MaximumUid=1999" in sddm,
)
print()
print(f"{passed} passed, {failed} failed")
sys.exit(1 if failed else 0)
PY
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""Compose the classroom's kdeglobals, colours included.
The panel came up light on a machine that had never had a colour scheme
applied to it, and dark on one that had. That is the whole bug in one
sentence: `ColorScheme=EkklesiaDark` is a label. KConfig reads the actual
colours out of [Colors:*] groups in kdeglobals, and nothing was putting
them there except a technician running classroom-theme once, by hand, on
a machine that then looked right for reasons the package did not own.
So the package ships the expanded palette in its own kdeglobals, at the
bottom of the cascade, where a room with no user configuration at all
finds it. One source of truth stays EkklesiaDark.colors: this composes,
it does not duplicate.
classroom-theme still works. plasma-apply-colorscheme writes the chosen
palette into the user's own kdeglobals, which sits ahead of this file, and
classroom-desktop-reset keeps a user palette whose name is an Ekklesia one.
tools/build-kdeglobals > somewhere/kdeglobals
"""
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
POLICY = ROOT / "config/plasma/xdg/kdeglobals"
SCHEME = ROOT / "config/plasma/colors/EkklesiaDark.colors"
# Name belongs to the scheme file, not to a session's configuration, and
# ColorScheme is already stated in the policy file above.
SKIP_IN_GENERAL = {"Name", "ColorScheme"}
def groups(text):
"""Yield (header, [lines]) for each group in a KConfig file."""
header = None
body = []
for line in text.splitlines():
if line.startswith("["):
if header is not None:
yield header, body
header, body = line, []
elif header is not None:
body.append(line)
if header is not None:
yield header, body
def policy_with(extra_general):
"""The policy file, with the scheme's stray [General] keys folded in.
Two [General] headers in one file is asking a parser a question it
should not be asked, so the keys go into the group that is already
there.
"""
lines = POLICY.read_text().rstrip("\n").splitlines()
out = []
inside = False
for line in lines:
if line.startswith("["):
if inside:
out.extend(extra_general)
inside = False
inside = line == "[General]"
out.append(line)
if inside:
out.extend(extra_general)
return "\n".join(out)
def main():
extra_general = []
for header, body in groups(SCHEME.read_text()):
if header != "[General]":
continue
extra_general = [
line
for line in body
if line.strip()
and not line.startswith("#")
and line.split("=", 1)[0].strip() not in SKIP_IN_GENERAL
]
out = [policy_with(extra_general), ""]
out.append(
"# ------------------------------------------------------------------\n"
"# Generated by tools/build-kdeglobals from EkklesiaDark.colors.\n"
"# Do not edit here: edit the scheme file and rebuild the package.\n"
"#\n"
"# These are the colours themselves. Without them a room with no user\n"
"# configuration gets KDE's built-in light palette however the scheme\n"
"# above is named.\n"
"# ------------------------------------------------------------------"
)
for header, body in groups(SCHEME.read_text()):
lines = [line for line in body if line.strip() and not line.startswith("#")]
if header == "[General]":
# Already folded into the policy file's own [General] above.
continue
if not lines:
continue
out.append("")
out.append(header)
out.extend(lines)
sys.stdout.write("\n".join(out) + "\n")
if __name__ == "__main__":
main()
+30 -1
View File
@@ -206,17 +206,32 @@ install -Dm644 \
# Not /etc/xdg: plasma-workspace already owns files by these names there. # Not /etc/xdg: plasma-workspace already owns files by these names there.
# The classroom's directory goes in front of it in XDG_CONFIG_DIRS instead. # The classroom's directory goes in front of it in XDG_CONFIG_DIRS instead.
for setting in kdeglobals kwinrc kwinrulesrc kscreenlockerrc ksmserverrc \ for setting in kwinrc kwinrulesrc kscreenlockerrc ksmserverrc \
krunnerrc plasmanotifyrc powermanagementprofilesrc; do krunnerrc plasmanotifyrc powermanagementprofilesrc; do
install -Dm644 \ install -Dm644 \
"$ROOT/config/plasma/xdg/${setting}" \ "$ROOT/config/plasma/xdg/${setting}" \
"$STAGE/etc/ekklesia-classroom/xdg/${setting}" "$STAGE/etc/ekklesia-classroom/xdg/${setting}"
done done
# kdeglobals is composed rather than copied: the session policy plus the
# dark palette itself. Naming a colour scheme does not produce one, and a
# room whose user profile had never had a scheme applied came up light.
install -d "$STAGE/etc/ekklesia-classroom/xdg"
"$ROOT/tools/build-kdeglobals" > "$STAGE/etc/ekklesia-classroom/xdg/kdeglobals"
chmod 0644 "$STAGE/etc/ekklesia-classroom/xdg/kdeglobals"
install -Dm644 \ install -Dm644 \
"$ROOT/config/environment/90-ekklesia-classroom.conf" \ "$ROOT/config/environment/90-ekklesia-classroom.conf" \
"$STAGE/etc/environment.d/90-ekklesia-classroom.conf" "$STAGE/etc/environment.d/90-ekklesia-classroom.conf"
# The one that actually reaches a session started by a display manager.
# startplasma sources this directory; PAM does not read environment.d, so
# without this file none of the settings above are in the cascade at all
# and the room comes up as stock Plasma. Measured, on a real machine.
install -Dm644 \
"$ROOT/config/plasma/env/zz-ekklesia-classroom.sh" \
"$STAGE/etc/xdg/plasma-workspace/env/zz-ekklesia-classroom.sh"
# The power button does nothing. Two layers, because while a session is # The power button does nothing. Two layers, because while a session is
# running PowerDevil holds logind's inhibitor and would overrule it. # running PowerDevil holds logind's inhibitor and would overrule it.
install -Dm644 \ install -Dm644 \
@@ -260,6 +275,14 @@ install -Dm644 \
"$ROOT/config/plasma/solid/ekklesia-dvd.desktop" \ "$ROOT/config/plasma/solid/ekklesia-dvd.desktop" \
"$STAGE/usr/share/solid/actions/ekklesia-dvd.desktop" "$STAGE/usr/share/solid/actions/ekklesia-dvd.desktop"
# Shadows the old notification daemon's D-Bus activation. Deactivating its
# systemd unit is not enough: D-Bus starts it on demand from a file
# mako-notifier owns, and the crash-recovery test on a migrated machine
# caught it doing exactly that while the panel was down.
install -Dm644 \
"$ROOT/config/dbus/fr.emersion.mako.service" \
"$STAGE/usr/local/share/dbus-1/services/fr.emersion.mako.service"
# Two marks the classroom draws itself, because nothing in Breeze says # Two marks the classroom draws itself, because nothing in Breeze says
# either thing: a lesson, and fetching a child's grown-up. # either thing: a lesson, and fetching a child's grown-up.
for icon in ekklesia-wonderink ekklesia-parent-call; do for icon in ekklesia-wonderink ekklesia-parent-call; do
@@ -310,6 +333,12 @@ install -Dm755 \
"$ROOT/scripts/classroom-theme" \ "$ROOT/scripts/classroom-theme" \
"$STAGE/usr/local/bin/classroom-theme" "$STAGE/usr/local/bin/classroom-theme"
# Converges a machine that used to run something else. Called from
# postinst on every install, and by hand by a technician.
install -Dm755 \
"$ROOT/scripts/classroom-migrate" \
"$STAGE/usr/local/bin/classroom-migrate"
install -Dm644 \ install -Dm644 \
"$ROOT/lib/classroom_audio.py" \ "$ROOT/lib/classroom_audio.py" \
"$STAGE/usr/local/lib/ekklesia-classroom/classroom_audio.py" "$STAGE/usr/local/lib/ekklesia-classroom/classroom_audio.py"
+205
View File
@@ -0,0 +1,205 @@
#!/bin/bash
# Is this machine the classroom?
#
# One list of properties, run against whatever the lab is currently
# holding. A fresh install and a machine upgraded from the old sway or
# Hyprland shell have to answer it identically — that is the whole point
# of the migration work, and the reason this is a file rather than a
# paragraph in a report.
#
# tools/lab/converged-check the properties
# tools/lab/converged-check --restart and again after a session restart
#
# Every check reads the running machine. Nothing here consults the
# repository, so a check cannot pass by agreeing with the source it came
# from.
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
LAB="$ROOT/tools/lab/lab"
NAME="${LAB_NAME:-classroom-v2-lab}"
PASS=0
FAIL=0
in_lab() { docker exec "$NAME" bash -c "$1" 2>/dev/null; }
# As the session itself reads configuration: the cascade of the running
# shell, not one this script made up.
session_config() {
local dirs
dirs=$(in_lab 'p=$(pgrep -x plasmashell | head -1); tr "\0" "\n" < /proc/$p/environ | grep "^XDG_CONFIG_DIRS=" | cut -d= -f2-')
docker exec -u classroom -e XDG_RUNTIME_DIR=/run/user/1000 -e HOME=/home/classroom \
-e LANG=C.UTF-8 -e "XDG_CONFIG_DIRS=$dirs" "$NAME" \
kreadconfig6 "$@" 2>/dev/null | tr -d '\r'
}
running() { in_lab "pgrep -x $1 | wc -l"; }
# Which process is actually answering org.freedesktop.Notifications. On a
# machine that still has mako installed this is the question that matters:
# the name is taken by whoever gets there first.
notification_owner() {
local pid
pid=$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- \
busctl --user --no-pager call org.freedesktop.DBus /org/freedesktop/DBus \
org.freedesktop.DBus GetConnectionUnixProcessID s org.freedesktop.Notifications' \
| awk '{print $2}')
[[ -n "$pid" ]] && in_lab "cat /proc/$pid/comm"
}
# logind answers for the running configuration, not for a file on disk.
logind_property() {
in_lab "busctl get-property org.freedesktop.login1 /org/freedesktop/login1 \
org.freedesktop.login1.Manager $1" | cut -d'"' -f2
}
APPLETS=/home/classroom/.config/plasma-org.kde.plasma.desktop-appletsrc
check() {
# check DESCRIPTION EXPECTED ACTUAL
if [[ "$2" == "$3" ]]; then
printf ' \033[32mPASS\033[0m %-52s %s\n' "$1" "$3"
PASS=$((PASS + 1))
else
printf ' \033[31mFAIL\033[0m %-52s got [%s] want [%s]\n' "$1" "$3" "$2"
FAIL=$((FAIL + 1))
fi
}
section() { printf '\n%s\n' "$1"; }
run_checks() {
section "the session"
check "kwin_wayland running" 1 "$(running kwin_wayland)"
check "plasmashell running" 1 "$(running plasmashell)"
section "the old shell is gone"
check "waybar not running" 0 "$(running waybar)"
check "mako not running" 0 "$(running mako)"
# The bracket keeps the pattern from matching the command that carries it.
check "classroom dock not running" 0 "$(in_lab 'pgrep -f "[c]lassroom-dock" | wc -l')"
# The lab's own outer compositor is sway, so the question is whether any
# sway is running that is not the harness's.
check "no sway but the lab harness's" 0 \
"$(in_lab 'pgrep -x sway | while read -r p; do tr "\0" " " < /proc/$p/cmdline | grep -q outer-sway.conf || echo x; done | grep -c x || true')"
check "Hyprland not running" 0 "$(running Hyprland)"
check "waybar activation removed" 0 \
"$(in_lab 'find /etc/systemd/user /etc/xdg/systemd/user /home/classroom/.config/systemd/user -name "waybar.service" -path "*.wants/*" 2>/dev/null | wc -l')"
check "mako activation removed" 0 \
"$(in_lab 'find /etc/systemd/user /etc/xdg/systemd/user /home/classroom/.config/systemd/user -name "mako.service" -path "*.wants/*" 2>/dev/null | wc -l')"
check "waybar masked" masked "$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- systemctl --user is-enabled waybar.service || true')"
check "mako masked" masked "$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- systemctl --user is-enabled mako.service || true')"
check "no login shell starting a compositor" 0 \
"$(in_lab 'grep -lE "exec (sway|Hyprland|start-hyprland|classroom-session)" /home/classroom/.bash_profile 2>/dev/null | wc -l')"
check "no tty1 autologin drop-in" 0 \
"$(in_lab 'ls /etc/systemd/system/[email protected]/autologin.conf 2>/dev/null | wc -l')"
check "no autostart entry for the old shell" 0 \
"$(in_lab 'grep -lE "^Exec=.*(waybar|mako|classroom-dock)" /home/classroom/.config/autostart/*.desktop 2>/dev/null | wc -l')"
section "device.conf"
check "COMPOSITOR removed" 0 "$(in_lab 'grep -c "^COMPOSITOR=" /etc/ekklesia-classroom/device.conf || true')"
check "not on the hyprland channel" 0 "$(in_lab 'grep -c "^CHANNEL=hyprland" /etc/ekklesia-classroom/device.conf || true')"
section "the configuration cascade"
check "classroom settings in the session's cascade" 1 \
"$(in_lab 'p=$(pgrep -x plasmashell | head -1); tr "\0" "\n" < /proc/$p/environ | grep -c "^XDG_CONFIG_DIRS=.*ekklesia-classroom/xdg"')"
check "look and feel authoritative" org.ekklesia.classroom.desktop \
"$(session_config --file kdeglobals --group KDE --key LookAndFeelPackage)"
check "and immutable" 1 \
"$(in_lab 'grep -c "^LookAndFeelPackage\[\$i\]=" /etc/ekklesia-classroom/xdg/kdeglobals')"
check "the palette is the packaged one" 20,28,32 \
"$(session_config --file kdeglobals --group 'Colors:Window' --key BackgroundNormal)"
section "the panel"
check "applet count" 10 "$(in_lab "grep -c '^plugin=' $APPLETS")"
check "Parent Call launcher" 1 \
"$(in_lab "grep -c 'ekklesia-parent-call.desktop' $APPLETS")"
check "quick launch, one widget" 1 \
"$(in_lab "grep -c '^plugin=org.kde.plasma.quicklaunch' $APPLETS")"
check "the applications are the room's" 1 \
"$(in_lab "grep -c 'launchers=applications:ekklesia-wonderink.desktop,applications:google-chrome.desktop' $APPLETS")"
check "network, volume, notifications, clock" 4 \
"$(in_lab "grep -cE '^plugin=org.kde.plasma.(networkmanagement|volume|notifications|digitalclock)$' $APPLETS")"
check "no kickoff" 0 "$(in_lab "grep -c 'plugin=org.kde.plasma.kickoff' $APPLETS")"
check "no pager" 0 "$(in_lab "grep -c 'plugin=org.kde.plasma.pager' $APPLETS")"
check "no system tray" 0 "$(in_lab "grep -c 'systemtray' $APPLETS")"
check "no discover or system settings" 0 \
"$(in_lab "grep -cE 'discover.desktop|systemsettings.desktop' $APPLETS")"
check "the packaged wallpaper" 1 \
"$(in_lab "grep -c 'Image=file:///usr/share/backgrounds/ekklesia/classroom-calm.png' $APPLETS")"
section "it will not strand a teacher"
check "no automatic lock" false "$(session_config --file kscreenlockerrc --group Daemon --key Autolock)"
check "no lock on resume" false "$(session_config --file kscreenlockerrc --group Daemon --key LockOnResume)"
check "the lock action is forbidden" false \
"$(session_config --file kdeglobals --group 'KDE Action Restrictions' --key action/lock_screen)"
check "power button does nothing" 0 \
"$(session_config --file powermanagementprofilesrc --group AC --group HandleButtonEvents --key powerButtonAction)"
check "no idle suspend, dim or blank configured" 0 \
"$(in_lab 'grep -cE "^\[(AC|Battery|LowBattery)\]\[(SuspendSession|DimDisplay|DPMSControl)\]" /etc/ekklesia-classroom/xdg/powermanagementprofilesrc || true')"
check "and the profiles are immutable" 3 \
"$(in_lab 'grep -cE "^\[(AC|Battery|LowBattery)\]\[\\\$i\]" /etc/ekklesia-classroom/xdg/powermanagementprofilesrc || true')"
check "logind ignores the power key" ignore "$(logind_property HandlePowerKey)"
check "logind ignores a long press" ignore "$(logind_property HandlePowerKeyLongPress)"
check "logind has no idle action" ignore "$(logind_property IdleAction)"
check "logind ignores the lid" ignore "$(logind_property HandleLidSwitch)"
section "lockdown still in force"
for key in run_command shell_access logout; do
check "$key" false "$(session_config --file kdeglobals --group 'KDE Action Restrictions' --key "$key")"
done
check "the panel cannot be edited" false \
"$(session_config --file kdeglobals --group 'KDE Action Restrictions' --key plasma/plasmashell/unlockedDesktop)"
section "autologin"
check "sddm enabled" enabled "$(in_lab 'systemctl is-enabled sddm.service || true')"
# Not a reading of the configuration: what SDDM actually did at the last
# boot of this machine. It authenticates the classroom through PAM and
# launches the session; the lab has no display hardware for the session to
# land on, so everything past this point is a hardware test.
check "sddm autologged the classroom in at boot" yes \
"$(in_lab 'journalctl -u sddm --no-pager 2>/dev/null | grep -q "Authentication for user .classroom. successful" && echo yes || echo no')"
check "and chose the Plasma session" yes \
"$(in_lab 'journalctl -u sddm --no-pager 2>/dev/null | grep -q "wayland-sessions/plasma.desktop. selected" && echo yes || echo no')"
check "autologin user" classroom \
"$(in_lab 'grep -A3 "^\[Autologin\]" /etc/sddm.conf.d/ekklesia-classroom.conf | sed -n "s/^User=//p"')"
check "autologin session" plasma \
"$(in_lab 'grep -A3 "^\[Autologin\]" /etc/sddm.conf.d/ekklesia-classroom.conf | sed -n "s/^Session=//p"')"
check "the session it names exists" 1 \
"$(in_lab 'ls /usr/share/wayland-sessions/plasma.desktop 2>/dev/null | wc -l')"
check "the classroom uid is inside the greeter's range" 1 \
"$(in_lab 'uid=$(id -u classroom); min=$(sed -n "s/^MinimumUid=//p" /etc/sddm.conf.d/ekklesia-classroom.conf); max=$(sed -n "s/^MaximumUid=//p" /etc/sddm.conf.d/ekklesia-classroom.conf); [ "$uid" -ge "$min" ] && [ "$uid" -le "$max" ] && echo 1 || echo 0')"
section "notifications belong to Plasma"
check "the old daemon cannot be activated" 1 \
"$(in_lab 'grep -c "^Exec=/bin/false" /usr/local/share/dbus-1/services/fr.emersion.mako.service 2>/dev/null || true')"
check "and Plasma owns the notification service" plasmashell "$(notification_owner)"
section "the room's own parts survived"
check "parent call installed" 1 "$(in_lab 'ls /usr/local/bin/classroom-parent-call 2>/dev/null | wc -l')"
check "parent call icon installed" 1 \
"$(in_lab 'ls /usr/share/icons/hicolor/scalable/apps/ekklesia-parent-call.svg 2>/dev/null | wc -l')"
check "wonderink installed" 1 "$(in_lab 'ls /usr/local/bin/classroom-wonderink 2>/dev/null | wc -l')"
check "the maintenance chord is registered" 1 \
"$(in_lab 'grep -c "^X-KDE-Shortcuts=Meta+Ctrl+Alt+Shift+K" /usr/share/applications/ekklesia-admin-terminal.desktop || true')"
check "no sudo rule for the classroom" 0 \
"$(in_lab 'grep -rl classroom /etc/sudoers /etc/sudoers.d 2>/dev/null | wc -l')"
check "the panel is supervised" always \
"$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- systemctl --user show plasma-plasmashell.service -p Restart --value')"
check "audio policy active" active \
"$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- systemctl --user is-active ekklesia-classroom-audio.service || true')"
}
run_checks
if [[ "${1:-}" == "--restart" ]]; then
section "and again after logging out and back in"
"$LAB" plasma-down >/dev/null
"$LAB" plasma-up >/dev/null
sleep 3
run_checks
fi
printf '\n%s passed, %s failed\n' "$PASS" "$FAIL"
[[ "$FAIL" -eq 0 ]]
+236
View File
@@ -0,0 +1,236 @@
#!/bin/bash
# The upgrade that failed, reproduced on purpose.
#
# A real classroom does not arrive new, and the clean-install test was
# passing on a machine that had never been anything else. This builds a
# machine that has: the Hyprland experiment package actually installed,
# with sway, waybar and mako behind it, a Waybar activated through
# graphical-session, an autostart entry, a device.conf still asking for a
# compositor that is gone, and a Plasma profile belonging to a different
# desktop. Then it installs the classroom package over the top and asks
# whether the room converged.
#
# tools/lab/dirty-upgrade [NEW.deb] [OLD.deb]
# CHECK_ARGS=--restart tools/lab/dirty-upgrade ask twice, either side
# of a logout
#
# The container is restarted between the install and the session, because
# a room takes its upgrade and reboots.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
LAB="$ROOT/tools/lab/lab"
NAME="${LAB_NAME:-classroom-v2-lab}"
NEW="${1:-$(ls -t "$ROOT"/dist/ekklesia-classroom_*_amd64.deb | grep -v '~' | head -1)}"
OLD="${2:-$ROOT/dist/ekklesia-classroom_0.1.45~hypr3_amd64.deb}"
step() { printf '\n\033[1m== %s\033[0m\n' "$1"; }
step "a fresh machine"
"$LAB" up >/dev/null
docker exec "$NAME" sh -c '
printf "#!/bin/sh\nexit 101\n" > /usr/sbin/policy-rc.d
chmod +x /usr/sbin/policy-rc.d
apt-get update -qq >/dev/null 2>&1
# The old package schedules a provisioning run that would go to
# backports for Hyprland. The lab is not testing that.
systemctl mask ekklesia-classroom-provision.service >/dev/null 2>&1'
step "the old classroom: $(basename "$OLD")"
docker exec "$NAME" sh -c "
DEBIAN_FRONTEND=noninteractive dpkg -i '/srv/classroom/${OLD#$ROOT/}' >/dev/null 2>&1
DEBIAN_FRONTEND=noninteractive apt-get -y -qq -f install >/dev/null 2>&1
DEBIAN_FRONTEND=noninteractive dpkg --configure ekklesia-classroom >/dev/null 2>&1
true" || true
docker exec "$NAME" dpkg-query -W -f=' installed: ${Version}\n' ekklesia-classroom
step "and everything a used machine accumulates"
docker exec -i "$NAME" bash <<'DIRT'
set -e
# Waybar and mako, activated the way the real room had them. Neither
# package ships a unit in trixie, so the units themselves are part of what
# somebody added — and one of the three activation directories is the one
# the broken room actually used.
for unit in waybar mako; do
cat > "/etc/systemd/user/$unit.service" <<UNIT
[Unit]
Description=$unit (left over from the old classroom shell)
PartOf=graphical-session.target
After=graphical-session.target
[Service]
ExecStart=/usr/bin/$unit
Restart=on-failure
[Install]
WantedBy=graphical-session.target
UNIT
done
for dir in /etc/systemd/user /etc/xdg/systemd/user \
/home/classroom/.config/systemd/user; do
mkdir -p "$dir/graphical-session.target.wants"
ln -sf /etc/systemd/user/waybar.service \
"$dir/graphical-session.target.wants/waybar.service"
ln -sf /etc/systemd/user/mako.service \
"$dir/graphical-session.target.wants/mako.service"
done
# An autostart entry as well, for a bar that is started two ways.
mkdir -p /home/classroom/.config/autostart
cat > /home/classroom/.config/autostart/waybar.desktop <<'ENTRY'
[Desktop Entry]
Type=Application
Name=Waybar
Exec=waybar
ENTRY
# The room still asking for a compositor that is gone, with a broker
# password in the same file to prove the migration does not rewrite it.
sed -i 's/^CHANNEL=.*/CHANNEL=hyprland/' /etc/ekklesia-classroom/device.conf
{
echo "COMPOSITOR=hyprland"
echo "HYPR_APPEARANCE=visual"
} >> /etc/ekklesia-classroom/device.conf
sed -i 's/^MQTT_HOST=.*/MQTT_HOST=broker.example.invalid/' /etc/ekklesia-classroom/device.conf
sed -i 's/^MQTT_PASSWORD=.*/MQTT_PASSWORD=keep-me-exactly/' /etc/ekklesia-classroom/device.conf
# A Plasma profile from a different desktop: someone else's palette, in
# both of the places that outrank the package.
mkdir -p /home/classroom/.config/kdedefaults
cat > /home/classroom/.config/kdedefaults/kdeglobals <<'KDE'
[General]
ColorScheme=BreezeLight
[Icons]
Theme=breeze
[KDE]
LookAndFeelPackage=org.kde.breeze.desktop
KDE
cat > /home/classroom/.config/kdeglobals <<'KDE'
[General]
ColorScheme=BreezeLight
[Colors:Window]
BackgroundNormal=239,240,241
ForegroundNormal=35,38,41
[KDE]
LookAndFeelPackage=org.kde.breeze.desktop
KDE
# And a stock panel, already built, of the kind the broken upgrade made.
cat > /home/classroom/.config/plasma-org.kde.plasma.desktop-appletsrc <<'PANEL'
[Containments][1]
plugin=org.kde.plasma.folder
[Containments][2]
plugin=org.kde.panel
[Containments][2][Applets][3]
plugin=org.kde.plasma.kickoff
[Containments][2][Applets][4]
plugin=org.kde.plasma.pager
[Containments][2][Applets][5]
plugin=org.kde.plasma.icontasks
[Containments][2][Applets][5][Configuration][General]
launchers=applications:systemsettings.desktop,applications:org.kde.discover.desktop,preferred://browser
[Containments][2][Applets][6]
plugin=org.kde.plasma.systemtray
PANEL
# A user-level power profile that would suspend the room.
cat > /home/classroom/.config/powermanagementprofilesrc <<'POWER'
[AC][SuspendSession]
idleTime=1800000
suspendType=1
[AC][DPMSControl]
idleTime=600
POWER
chown -R classroom:classroom /home/classroom/.config
echo " dirt in place"
DIRT
step "the new classroom: $(basename "$NEW")"
docker exec "$NAME" sh -c '
systemctl unmask ekklesia-classroom-provision.service >/dev/null 2>&1 || true'
docker exec "$NAME" sh -c "
DEBIAN_FRONTEND=noninteractive dpkg -i '/srv/classroom/${NEW#$ROOT/}' 2>&1 \
| grep -E '^\[classroom-migrate\]' || true
DEBIAN_FRONTEND=noninteractive apt-get -y -qq -f install >/dev/null 2>&1
DEBIAN_FRONTEND=noninteractive dpkg --configure ekklesia-classroom >/dev/null 2>&1
rm -f /usr/sbin/policy-rc.d
true" || true
docker exec "$NAME" dpkg-query -W -f=' installed: ${Version} ${db:Status-Abbrev}\n' ekklesia-classroom
step "and the room reboots"
"$LAB" reboot
step "the session"
"$LAB" session >/dev/null
sleep 6
step "did it converge?"
converged=0
"$ROOT/tools/lab/converged-check" ${CHECK_ARGS:-} || converged=1
step "and again, installed a second time over itself"
# An update that changes nothing must change nothing. The migration runs
# again here, over a machine that is already converged.
docker exec "$NAME" sh -c "
DEBIAN_FRONTEND=noninteractive dpkg -i '/srv/classroom/${NEW#$ROOT/}' >/dev/null 2>&1
true" || true
before=$(docker exec "$NAME" sh -c 'cat /etc/ekklesia-classroom/device.conf | sha256sum')
"$LAB" plasma-down >/dev/null
"$LAB" plasma-up >/dev/null
sleep 4
after=$(docker exec "$NAME" sh -c 'cat /etc/ekklesia-classroom/device.conf | sha256sum')
[[ "$before" == "$after" ]] \
&& echo " device.conf unchanged by the second install" \
|| { echo " DEVICE.CONF CHANGED ON REINSTALL"; converged=1; }
"$ROOT/tools/lab/converged-check" || converged=1
step "and the room's own theme choice survives a logout"
# Deleting the look and feel defaults every login was measured to make
# Plasma re-apply the look and feel and throw the room's chosen palette
# away with it. This is the check that stops that coming back.
# With the session's own cascade. plasma-apply-colorscheme writes the
# chosen name into ~/.config/kdedefaults, which is in XDG_CONFIG_DIRS for
# a session and not for a bare command, so asking without it reports
# nothing and means nothing.
theme() {
local dirs
dirs=$(docker exec "$NAME" bash -c 'p=$(pgrep -x plasmashell | head -1); tr "\0" "\n" < /proc/$p/environ | grep "^XDG_CONFIG_DIRS=" | cut -d= -f2-')
docker exec -u classroom -e XDG_RUNTIME_DIR=/run/user/1000 \
-e DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \
-e WAYLAND_DISPLAY=wayland-0 -e "XDG_CONFIG_DIRS=$dirs" "$NAME" \
/usr/local/bin/classroom-theme "$1" 2>/dev/null | tail -1
}
theme light >/dev/null
"$LAB" plasma-down >/dev/null
"$LAB" plasma-up >/dev/null
sleep 4
chosen=$(theme show)
if [[ "$chosen" == EkklesiaLight ]]; then
echo " light survived the logout"
else
echo " THE ROOM'S THEME CHOICE WAS LOST: $chosen"
converged=1
fi
theme dark >/dev/null
echo " back to $(theme show)"
step "and it still recovers"
"$ROOT/tools/lab/recovery-check" || converged=1
exit "$converged"
+10 -3
View File
@@ -27,9 +27,11 @@ docker exec "$NAME" sh -c "
state=$(docker exec "$NAME" dpkg-query -W -f='${db:Status-Abbrev}' ekklesia-classroom) state=$(docker exec "$NAME" dpkg-query -W -f='${db:Status-Abbrev}' ekklesia-classroom)
echo "package state: $state" echo "package state: $state"
# environment.d is read when the user manager starts, so it starts after. # And then the machine is restarted, the way a room takes an update: a
docker exec "$NAME" systemctl restart [email protected]e # new PID 1, a new user manager, SDDM autologging the classroom in. The
sleep 4 # session environment, the logind drop-in and the user services are all
# things that are only read at boot.
"$LAB" reboot
"$LAB" session >/dev/null "$LAB" session >/dev/null
sleep 8 sleep 8
@@ -50,3 +52,8 @@ check "panel restart policy" "$(docker exec -u classroom -e XDG_RUNTIME_DIR=/run
check "audio policy" "$(docker exec -u classroom -e XDG_RUNTIME_DIR=/run/user/1000 \ check "audio policy" "$(docker exec -u classroom -e XDG_RUNTIME_DIR=/run/user/1000 \
-e DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus "$NAME" \ -e DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus "$NAME" \
systemctl --user is-active ekklesia-classroom-audio.service || true)" systemctl --user is-active ekklesia-classroom-audio.service || true)"
# The same list of properties the upgraded machine has to satisfy. A
# fresh install and a migrated one are supposed to be indistinguishable
# from a teacher's chair, so they are asked the same questions.
"$ROOT/tools/lab/converged-check" ${CHECK_ARGS:-}
+14 -1
View File
@@ -12,16 +12,26 @@ SRC="${1:-/srv/classroom}"
# A directory of the classroom's own, ahead of /etc/xdg in # A directory of the classroom's own, ahead of /etc/xdg in
# XDG_CONFIG_DIRS. plasma-workspace owns files by these names in /etc/xdg # XDG_CONFIG_DIRS. plasma-workspace owns files by these names in /etc/xdg
# and two packages cannot own one path. # and two packages cannot own one path.
for setting in kdeglobals kwinrc kwinrulesrc kscreenlockerrc ksmserverrc \ for setting in kwinrc kwinrulesrc kscreenlockerrc ksmserverrc \
krunnerrc plasmanotifyrc powermanagementprofilesrc; do krunnerrc plasmanotifyrc powermanagementprofilesrc; do
install -Dm644 "$SRC/config/plasma/xdg/$setting" \ install -Dm644 "$SRC/config/plasma/xdg/$setting" \
"/etc/ekklesia-classroom/xdg/$setting" "/etc/ekklesia-classroom/xdg/$setting"
rm -f "/etc/xdg/$setting.ekklesia-old" rm -f "/etc/xdg/$setting.ekklesia-old"
done done
# Composed, not copied — the policy and the palette. See tools/build-kdeglobals.
install -d /etc/ekklesia-classroom/xdg
"$SRC/tools/build-kdeglobals" > /etc/ekklesia-classroom/xdg/kdeglobals
chmod 0644 /etc/ekklesia-classroom/xdg/kdeglobals
install -Dm644 "$SRC/config/environment/90-ekklesia-classroom.conf" \ install -Dm644 "$SRC/config/environment/90-ekklesia-classroom.conf" \
/etc/environment.d/90-ekklesia-classroom.conf /etc/environment.d/90-ekklesia-classroom.conf
# What actually puts that directory into a session started by a display
# manager. startplasma sources this; PAM never reads environment.d.
install -Dm644 "$SRC/config/plasma/env/zz-ekklesia-classroom.sh" \
/etc/xdg/plasma-workspace/env/zz-ekklesia-classroom.sh
install -Dm644 "$SRC/config/systemd/logind-ekklesia-classroom.conf" \ install -Dm644 "$SRC/config/systemd/logind-ekklesia-classroom.conf" \
/etc/systemd/logind.conf.d/ekklesia-classroom.conf /etc/systemd/logind.conf.d/ekklesia-classroom.conf
@@ -53,6 +63,9 @@ done
install -Dm644 "$SRC/config/plasma/solid/ekklesia-dvd.desktop" \ install -Dm644 "$SRC/config/plasma/solid/ekklesia-dvd.desktop" \
/usr/share/solid/actions/ekklesia-dvd.desktop /usr/share/solid/actions/ekklesia-dvd.desktop
install -Dm644 "$SRC/config/dbus/fr.emersion.mako.service" \
/usr/local/share/dbus-1/services/fr.emersion.mako.service
for icon in "$SRC"/config/icons/ekklesia-*.svg; do for icon in "$SRC"/config/icons/ekklesia-*.svg; do
[ -e "$icon" ] || continue [ -e "$icon" ] || continue
install -Dm644 "$icon" \ install -Dm644 "$icon" \
+76 -3
View File
@@ -35,6 +35,17 @@ outer() {
session_env -e WAYLAND_DISPLAY="$OUTER" -e SWAYSOCK="$sock" "$NAME" "$@" session_env -e WAYLAND_DISPLAY="$OUTER" -e SWAYSOCK="$sock" "$NAME" "$@"
} }
# NOTE ON XDG_CONFIG_DIRS.
#
# This harness used to hand the session
# XDG_CONFIG_DIRS=/etc/ekklesia-classroom/xdg:/etc/xdg, and every result it
# produced was therefore a result about a machine somebody had already
# configured by hand. A real session is started by SDDM through PAM, which
# does not read /etc/environment.d, and the classroom's settings were not
# in the cascade at all: the first real upgrade came up as stock Debian
# Plasma with a lock screen. The variable is deliberately not set here any
# more. The package has to put it there, the way it has to on a machine.
# Talk to the Plasma session under test. # Talk to the Plasma session under test.
inner() { inner() {
session_env -e WAYLAND_DISPLAY=wayland-0 -e XDG_CURRENT_DESKTOP=KDE \ session_env -e WAYLAND_DISPLAY=wayland-0 -e XDG_CURRENT_DESKTOP=KDE \
@@ -74,6 +85,55 @@ down)
echo "lab $NAME is down" echo "lab $NAME is down"
;; ;;
reboot)
# As close to a room being switched off and on as a container gets:
# the same filesystem, a new PID 1, a new user manager, and every
# generator and drop-in read again from scratch. The migration work
# needs this, because half of what an upgrade changes is only read at
# boot.
docker restart "$NAME" >/dev/null
for _ in $(seq 1 90); do
state=$(docker exec "$NAME" systemctl is-system-running 2>/dev/null || true)
[[ "$state" == running || "$state" == degraded ]] && break
sleep 1
done
for _ in $(seq 1 30); do
docker exec "$NAME" test -S /run/user/1000/bus && break
sleep 1
done
docker exec "$NAME" ln -sf /usr/bin/qdbus6 /usr/local/bin/qdbus
# 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
# path, not a reading of a configuration file — so the attempt is left
# to happen and its evidence stays in the journal. It is then stopped,
# because the harness runs its own session on the nested compositor
# and two sessions cannot both own the keyboard.
for _ in $(seq 1 30); do
docker exec "$NAME" journalctl -u sddm --no-pager 2>/dev/null \
| grep -q "Session started" && break
sleep 1
done
docker exec "$NAME" systemctl stop sddm >/dev/null 2>&1 || true
docker exec "$NAME" pkill -f sddm-helper >/dev/null 2>&1 || true
# -f, not -x. "startplasma-wayland" is longer than the fifteen
# characters a process name has, so -x silently matches nothing and
# SDDM's session was left winding down underneath the harness's own:
# it tore down graphical-session.target sixteen seconds later and took
# the panel with it, which cost an afternoon to understand.
docker exec "$NAME" pkill -f startplasma-wayland >/dev/null 2>&1 || true
docker exec "$NAME" pkill -f plasma-dbus-run-session >/dev/null 2>&1 || true
for _ in $(seq 1 20); do
state=$(docker exec -u classroom -e XDG_RUNTIME_DIR=/run/user/1000 "$NAME" \
systemctl --user is-active graphical-session.target 2>/dev/null || true)
[[ "$state" != active ]] && break
sleep 1
done
sleep 2
echo "lab $NAME rebooted ($(docker exec "$NAME" systemctl is-system-running 2>/dev/null || true))"
;;
outer-up) outer-up)
# A headless compositor has no input devices, and a wl_seat with no # A headless compositor has no input devices, and a wl_seat with no
# pointer capability never gives the session under test a pointer to # pointer capability never gives the session under test a pointer to
@@ -103,7 +163,6 @@ plasma-up)
-e WAYLAND_DISPLAY="$OUTER" -e XDG_SESSION_TYPE=wayland \ -e WAYLAND_DISPLAY="$OUTER" -e XDG_SESSION_TYPE=wayland \
-e XDG_CURRENT_DESKTOP=KDE -e LIBGL_ALWAYS_SOFTWARE=1 \ -e XDG_CURRENT_DESKTOP=KDE -e LIBGL_ALWAYS_SOFTWARE=1 \
-e QT_LOGGING_RULES="kwin_scripting.debug=true" \ -e QT_LOGGING_RULES="kwin_scripting.debug=true" \
-e XDG_CONFIG_DIRS=/etc/ekklesia-classroom/xdg:/etc/xdg \
-d "$NAME" sh -c 'exec startplasma-wayland > /tmp/plasma.log 2>&1' -d "$NAME" sh -c 'exec startplasma-wayland > /tmp/plasma.log 2>&1'
for _ in $(seq 1 60); do for _ in $(seq 1 60); do
docker exec "$NAME" pgrep -x plasmashell >/dev/null 2>&1 && break docker exec "$NAME" pgrep -x plasmashell >/dev/null 2>&1 && break
@@ -114,9 +173,22 @@ plasma-up)
;; ;;
plasma-down) plasma-down)
# A logout, not a killing. plasmashell and kwin are both services of
# the user manager, and killing them only makes it start them again —
# the compositor came straight back holding wayland-0 and the next
# session could not start at all. The workspace target has to go down
# first, and this is what a real logout does too.
session_env "$NAME" systemctl --user stop plasma-workspace.target \
>/dev/null 2>&1 || true
docker exec "$NAME" pkill -u classroom -f startplasma-wayland || true docker exec "$NAME" pkill -u classroom -f startplasma-wayland || true
docker exec "$NAME" pkill -u classroom -x plasmashell || true session_env "$NAME" systemctl --user stop graphical-session.target \
docker exec "$NAME" pkill -u classroom -x kwin_wayland || true >/dev/null 2>&1 || true
for _ in $(seq 1 20); do
docker exec "$NAME" pgrep -x kwin_wayland >/dev/null 2>&1 || break
sleep 1
done
docker exec "$NAME" pkill -u classroom -x kwin_wayland >/dev/null 2>&1 || true
docker exec "$NAME" pkill -u classroom -x plasmashell >/dev/null 2>&1 || true
sleep 2 sleep 2
echo "plasma session down" echo "plasma session down"
;; ;;
@@ -200,6 +272,7 @@ usage: tools/lab/lab COMMAND
build build the lab image build build the lab image
up start the lab container (systemd inside) up start the lab container (systemd inside)
down remove it down remove it
reboot restart the machine, let SDDM autologin, then stand down
session outer compositor + a Plasma session on top of it session outer compositor + a Plasma session on top of it
outer-up just the outer compositor outer-up just the outer compositor
plasma-up just the Plasma session plasma-up just the Plasma session