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]>
@@ -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
|
||||
@@ -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
|
||||
@@ -48,7 +48,18 @@ XftSubPixel=rgb
|
||||
|
||||
[KDE]
|
||||
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
|
||||
AnimationDurationFactor=0.5
|
||||
|
||||
|
||||
@@ -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
|
||||
# running, so the logind drop-in on its own would be overruled by
|
||||
# whatever this says. 0 is no action.
|
||||
# Two failures on real hardware are behind this file. The power button put
|
||||
# a room to sleep; and a machine left alone came back to a password prompt
|
||||
# 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,
|
||||
# but a profile that exists and disagrees is worse than one that does not
|
||||
# exist at all.
|
||||
#
|
||||
# 0 is no action.
|
||||
|
||||
[AC][$i]
|
||||
icon=battery-charging
|
||||
|
||||
[AC][HandleButtonEvents][$i]
|
||||
powerButtonAction=0
|
||||
lidAction=0
|
||||
triggerLidActionWhenExternalMonitorPresent=false
|
||||
|
||||
[Battery][$i]
|
||||
icon=battery-060
|
||||
|
||||
[Battery][HandleButtonEvents][$i]
|
||||
powerButtonAction=0
|
||||
lidAction=0
|
||||
triggerLidActionWhenExternalMonitorPresent=false
|
||||
|
||||
[LowBattery][$i]
|
||||
icon=battery-low
|
||||
|
||||
[LowBattery][HandleButtonEvents][$i]
|
||||
powerButtonAction=0
|
||||
lidAction=0
|
||||
|
||||
@@ -19,6 +19,11 @@ DisplayServer=wayland
|
||||
InputMethod=
|
||||
|
||||
[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
|
||||
MaximumUid=1999
|
||||
HideShells=/usr/sbin/nologin,/bin/false
|
||||
|
||||
@@ -32,3 +32,9 @@ HandleRebootKeyLongPress=ignore
|
||||
HandleLidSwitch=ignore
|
||||
HandleLidSwitchExternalPower=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
|
||||
|
||||
@@ -937,6 +937,165 @@ ekklesia-classroom-tv.service small, supervised, ours
|
||||
adapters arrive today; the commands get measured before anything is
|
||||
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
|
||||
|
||||
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
|
||||
typed through a lab-only `--child` argument. **Type a name on the
|
||||
bench before a Sunday.**
|
||||
4. **SDDM is configured and untested.** It cannot be exercised in a
|
||||
container, and it is the thing that starts the session. If the config is
|
||||
wrong the room boots to nothing. This is the first thing to check on a
|
||||
VM or a bench machine, and it is why the checklist below starts there.
|
||||
4. **SDDM has never put a picture on a screen.** More is known than
|
||||
before: in the lab it really does autologin, authenticating the
|
||||
classroom through PAM and launching the Plasma session — that is now
|
||||
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
|
||||
well tested; applying it is not. A wrong `pactl set-card-profile` on
|
||||
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 —
|
||||
verified over D-Bus — but key injection cannot reach the nested
|
||||
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
|
||||
Konsole refuses to open a shell under the classroom's
|
||||
`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.
|
||||
* **Long-Escape investigated and recommended against**, with the two
|
||||
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:
|
||||
|
||||
@@ -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
|
||||
answer, and each of them is a Sunday-morning path. Add the power
|
||||
button and the maintenance chord to that list.
|
||||
2. Publish `0.1.45~plasma1` to a channel of its own, the way the Hyprland
|
||||
milestone did, so a stable or canary room can never be offered it.
|
||||
3. Bench-test SDDM: autologin, and that ending the session brings the
|
||||
classroom back rather than a shell.
|
||||
4. When the CEC adapters have been exercised, replace the computer volume
|
||||
2. Publish the v2 channel. `classroom-migrate` already moves a migrated
|
||||
room onto `CHANNEL=classroom-v2`, and nothing serves that manifest
|
||||
yet, so those machines are taking no updates at all. This is now the
|
||||
first packaging job rather than a tidy-up.
|
||||
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
|
||||
*Television control, and where CEC will fit*.
|
||||
5. Live View, when the rest is quiet.
|
||||
6. Live View, when the rest is quiet.
|
||||
|
||||
@@ -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-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
|
||||
|
||||
| | |
|
||||
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 253 KiB |
|
After Width: | Height: | Size: 393 KiB |
|
After Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 251 KiB After Width: | Height: | Size: 253 KiB |
@@ -8,13 +8,6 @@ if ! id classroom >/dev/null 2>&1; then
|
||||
classroom
|
||||
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
|
||||
# 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
|
||||
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
|
||||
|
||||
# The power button stops working immediately rather than at the next
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
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
|
||||
# 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
|
||||
# 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
|
||||
rm -f "$CONFIG"
|
||||
echo "classroom-desktop-reset: panel will be rebuilt from the layout"
|
||||
say "panel will be rebuilt from the layout"
|
||||
else
|
||||
echo "classroom-desktop-reset: no panel configuration to clear"
|
||||
say "no panel configuration to clear"
|
||||
fi
|
||||
|
||||
# The icon cache remembers a name it once failed to find, which outlives
|
||||
# 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
|
||||
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
check("settings install to the classroom's own directory",
|
||||
"$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",
|
||||
"XDG_CONFIG_DIRS=/etc/ekklesia-classroom/xdg:/etc/xdg" in
|
||||
(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)
|
||||
check("PowerDevil: no action on a lid either",
|
||||
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",
|
||||
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",
|
||||
"/etc/systemd/logind.conf.d/ekklesia-classroom.conf" in build)
|
||||
check("the PowerDevil profile is packaged",
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -206,17 +206,32 @@ install -Dm644 \
|
||||
|
||||
# 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.
|
||||
for setting in kdeglobals kwinrc kwinrulesrc kscreenlockerrc ksmserverrc \
|
||||
for setting in kwinrc kwinrulesrc kscreenlockerrc ksmserverrc \
|
||||
krunnerrc plasmanotifyrc powermanagementprofilesrc; do
|
||||
install -Dm644 \
|
||||
"$ROOT/config/plasma/xdg/${setting}" \
|
||||
"$STAGE/etc/ekklesia-classroom/xdg/${setting}"
|
||||
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 \
|
||||
"$ROOT/config/environment/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
|
||||
# running PowerDevil holds logind's inhibitor and would overrule it.
|
||||
install -Dm644 \
|
||||
@@ -260,6 +275,14 @@ install -Dm644 \
|
||||
"$ROOT/config/plasma/solid/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
|
||||
# either thing: a lesson, and fetching a child's grown-up.
|
||||
for icon in ekklesia-wonderink ekklesia-parent-call; do
|
||||
@@ -310,6 +333,12 @@ install -Dm755 \
|
||||
"$ROOT/scripts/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 \
|
||||
"$ROOT/lib/classroom_audio.py" \
|
||||
"$STAGE/usr/local/lib/ekklesia-classroom/classroom_audio.py"
|
||||
|
||||
@@ -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 ]]
|
||||
@@ -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"
|
||||
@@ -27,9 +27,11 @@ docker exec "$NAME" sh -c "
|
||||
state=$(docker exec "$NAME" dpkg-query -W -f='${db:Status-Abbrev}' ekklesia-classroom)
|
||||
echo "package state: $state"
|
||||
|
||||
# environment.d is read when the user manager starts, so it starts after.
|
||||
docker exec "$NAME" systemctl restart [email protected]e
|
||||
sleep 4
|
||||
# And then the machine is restarted, the way a room takes an update: a
|
||||
# new PID 1, a new user manager, SDDM autologging the classroom in. The
|
||||
# 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
|
||||
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 \
|
||||
-e DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus "$NAME" \
|
||||
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:-}
|
||||
|
||||
@@ -12,16 +12,26 @@ SRC="${1:-/srv/classroom}"
|
||||
# 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
|
||||
# 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
|
||||
install -Dm644 "$SRC/config/plasma/xdg/$setting" \
|
||||
"/etc/ekklesia-classroom/xdg/$setting"
|
||||
rm -f "/etc/xdg/$setting.ekklesia-old"
|
||||
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" \
|
||||
/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" \
|
||||
/etc/systemd/logind.conf.d/ekklesia-classroom.conf
|
||||
|
||||
@@ -53,6 +63,9 @@ done
|
||||
install -Dm644 "$SRC/config/plasma/solid/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
|
||||
[ -e "$icon" ] || continue
|
||||
install -Dm644 "$icon" \
|
||||
|
||||
@@ -35,6 +35,17 @@ outer() {
|
||||
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.
|
||||
inner() {
|
||||
session_env -e WAYLAND_DISPLAY=wayland-0 -e XDG_CURRENT_DESKTOP=KDE \
|
||||
@@ -74,6 +85,55 @@ 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)
|
||||
# A headless compositor has no input devices, and a wl_seat with no
|
||||
# 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 XDG_CURRENT_DESKTOP=KDE -e LIBGL_ALWAYS_SOFTWARE=1 \
|
||||
-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'
|
||||
for _ in $(seq 1 60); do
|
||||
docker exec "$NAME" pgrep -x plasmashell >/dev/null 2>&1 && break
|
||||
@@ -114,9 +173,22 @@ plasma-up)
|
||||
;;
|
||||
|
||||
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 -x plasmashell || true
|
||||
docker exec "$NAME" pkill -u classroom -x kwin_wayland || true
|
||||
session_env "$NAME" systemctl --user stop graphical-session.target \
|
||||
>/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
|
||||
echo "plasma session down"
|
||||
;;
|
||||
@@ -200,6 +272,7 @@ usage: tools/lab/lab COMMAND
|
||||
build build the lab image
|
||||
up start the lab container (systemd inside)
|
||||
down remove it
|
||||
reboot restart the machine, let SDDM autologin, then stand down
|
||||
session outer compositor + a Plasma session on top of it
|
||||
outer-up just the outer compositor
|
||||
plasma-up just the Plasma session
|
||||
|
||||