feat: give every room the broker, one scale, and its own panel back
Little Waders took the Plasma build and reported what a container cannot. A disc player on a machine with no optical drive, a clock nobody at the back of the room could read, a browser at half the size of the panel around it, and Parent Call with no broker to call. The broker is the same for every classroom, so it is a default now and not something to fill in room by room. classroom-migrate brings an existing room to the same place without touching its name, its username or its password, and Parent Call distinguishes a broker it cannot reach from one that was never set up: those send somebody to look at completely different things. The disc player's detection was right and ran too late — alongside the session rather than before it, and the panel is built from the layout the moment a session starts. It runs before the display manager now, and again from udev when a drive is plugged in. UI_SCALE had a ceiling of 1.5, which cannot express a 4K television; a clock pinned at 15 points, which is the same size at every scale; and a browser that was never told the number at all. All three are fixed and the room doubles together at 200%. This also brings 0.1.47's work into the repository, where it had only ever existed in a build directory under /tmp: the hidden display dialog, its authenticated writer, the polkit action, and the church's own wallpaper. One piece of it is deliberately left out. The custom volume display needed a shell package of our own, and with one selected plasmashell builds no panel at all — measured twice, with that build's own files, on a classroom that deletes its panel configuration at every login by design. The lab connected to the real church broker the moment the package defaulted to it. Nothing was left behind, which was checked, but the guard that should have stopped it had never worked: it used iptables, which the lab image does not have, and probed with /dev/tcp under dash, where that always fails. Both reported success. Co-Authored-By: Claude Opus 5 <[email protected]>
@@ -1,21 +1,26 @@
|
||||
{
|
||||
"DefaultBrowserSettingEnabled": false,
|
||||
"PromotionalTabsEnabled": false,
|
||||
"MetricsReportingEnabled": false,
|
||||
"BrowserSignin": 0,
|
||||
"SyncDisabled": true,
|
||||
"SigninInterceptionEnabled": false,
|
||||
"BackgroundModeEnabled": false,
|
||||
"BrowserAddPersonEnabled": false,
|
||||
"BrowserGuestModeEnabled": false,
|
||||
"ProfilePickerOnStartupAvailability": 1,
|
||||
"PasswordManagerEnabled": false,
|
||||
"PasswordLeakDetectionEnabled": false,
|
||||
"BackgroundModeEnabled": false,
|
||||
"DisablePrintPreview": true,
|
||||
"BrowserSignin": 0,
|
||||
"CookiesSessionOnlyForUrls": [
|
||||
"[*.]wonderink.org"
|
||||
],
|
||||
"ShowHomeButton": true,
|
||||
"DefaultBrowserSettingEnabled": false,
|
||||
"DefaultNotificationsSetting": 2,
|
||||
"DisablePrintPreview": true,
|
||||
"HomepageIsNewTabPage": false,
|
||||
"HomepageLocation": "https://www.youtube.com/"
|
||||
"HomepageLocation": "https://www.youtube.com/",
|
||||
"MetricsReportingEnabled": false,
|
||||
"NotificationsAllowedForUrls": [],
|
||||
"NotificationsBlockedForUrls": [
|
||||
"*"
|
||||
],
|
||||
"PasswordLeakDetectionEnabled": false,
|
||||
"PasswordManagerEnabled": false,
|
||||
"ProfilePickerOnStartupAvailability": 1,
|
||||
"PromotionalTabsEnabled": false,
|
||||
"ShowHomeButton": true,
|
||||
"SigninInterceptionEnabled": false,
|
||||
"SyncDisabled": true
|
||||
}
|
||||
|
||||
@@ -1,33 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<!-- The comment lives inside <svg>, not above it. gdk-pixbuf
|
||||
recognises an SVG by sniffing the first bytes of the file, and a
|
||||
licence-length comment before the opening tag pushes it out of
|
||||
that window: every GTK program then reports "couldn't recognize
|
||||
the image file format" while Qt renders it happily. Measured on
|
||||
both of the classroom's own icons. -->
|
||||
<!-- Wonder Ink.
|
||||
|
||||
A stand-in for the product's own mark, which the classroom does not
|
||||
have a licensed asset for. It is a filled colour tile rather than a
|
||||
line drawing on purpose: the room already learned, the hard way, that
|
||||
a monochrome glyph on a bar is read as a decoration and not as an
|
||||
application. Somebody asked whether the old browser glyph meant "the
|
||||
Internet". A tile with its own colour does not get that question. -->
|
||||
<defs>
|
||||
<linearGradient id="tile" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#4FB3A8"/>
|
||||
<stop offset="0.55" stop-color="#3E93A8"/>
|
||||
<stop offset="1" stop-color="#7C6FC4"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="6" y="6" width="116" height="116" rx="28" fill="url(#tile)"/>
|
||||
<path d="M64 30c0 0 26 28 26 44a26 26 0 0 1-52 0c0-16 26-44 26-44z"
|
||||
fill="#FFFFFF" fill-opacity="0.94"/>
|
||||
<path d="M50 76a14 14 0 0 0 10 13" stroke="#3E93A8" stroke-width="5"
|
||||
stroke-linecap="round" fill="none" opacity="0.55"/>
|
||||
<g fill="#FFFFFF" opacity="0.9">
|
||||
<path d="M96 34l2.6 6.4L105 43l-6.4 2.6L96 52l-2.6-6.4L87 43l6.4-2.6z"/>
|
||||
<circle cx="34" cy="40" r="3.2"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<title>Wonder Ink</title>
|
||||
<!-- The Wonder Ink mark, as supplied, on a square canvas.
|
||||
|
||||
A panel icon slot is square and the mark is 65.67 by 46.68, so it is
|
||||
fitted to the width and centred rather than stretched: the
|
||||
transform below is the only thing added to the artwork, and the
|
||||
paths are untouched.
|
||||
|
||||
The comment is inside the root element on purpose. A comment
|
||||
before it stops gdk-pixbuf recognising the file as SVG at all,
|
||||
which took two classroom icons out of the panel once already. -->
|
||||
<g transform="translate(0 9.254) scale(0.97457)">
|
||||
<path d="M64.54 45.7l-31.4 1a3.2 3.2 0 01-2.93-3.14l.13-34.75 32.28 1.49a3.23 3.23 0 013 3.51z" fill="#003b5c"></path><path d="M38.15 37l-32.61.91A3.18 3.18 0 012.42 35L0 0l36.25.85a3.21 3.21 0 013.26 3.3z" fill="#e48043"></path><path fill="#85b36d" d="M45.67 13.92l6.66.55.78 27.54h-7.23l-.21-28.09z"></path><path d="M4.81 4.46h4.75l3.52 17.8c.2 1 .36 2 .36 2s.12-1 .28-2L15.13 7h7.14l1.44 15.3c.15 1 .27 2 .27 2s.16-1 .36-2l2.3-16.34 5.11.21c1.1 0 2.52 1.87 2.18 2.92l-6.62 23.4h-6.58L19 22.26c-.2-1.11-.28-1.74-.28-1.74s-.08.63-.28 1.74l-1.75 10.23h-3.37a1.28 1.28 0 01-1.22-.9z" fill="#efc848"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,8 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Classroom display size
|
||||
Exec=/usr/local/bin/classroom-display-settings
|
||||
Icon=preferences-desktop-display
|
||||
Terminal=false
|
||||
NoDisplay=true
|
||||
X-KDE-Shortcuts=Meta+Ctrl+Alt+Shift+D
|
||||
@@ -0,0 +1,6 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Classroom display size
|
||||
Exec=/usr/local/bin/classroom-scale
|
||||
OnlyShowIn=KDE;
|
||||
NoDisplay=true
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
The card every Plasma popup in the classroom is drawn on: notifications,
|
||||
the volume display, the panel's own popups.
|
||||
|
||||
A Plasma style is the supported way to change this, and it is the reason
|
||||
no QML was replaced to do it. A style is a set of pictures: anything this
|
||||
package does not provide comes from Breeze, so the worst this file can do
|
||||
is look wrong, never fail to appear. Replacing the notification plasmoid's
|
||||
QML would have been the other route, and a QML error there is a classroom
|
||||
with no notifications at all.
|
||||
|
||||
Nine named pieces, which is what Plasma's FrameSvg expects: four corners,
|
||||
four edges and a middle. The corners are the whole point — a 14px radius
|
||||
instead of Breeze's, so a card on a television reads as a card rather than
|
||||
as a panel that has come loose.
|
||||
|
||||
The colour is not in here. ColorScheme-Background is replaced at runtime
|
||||
by the session's own scheme, so this follows classroom-theme between dark
|
||||
and light without a second file.
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.1">
|
||||
<defs>
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Background { color: #141c20; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<g class="ColorScheme-Background" fill="currentColor">
|
||||
<path id="topleft" d="M 0,14 A 14,14 0 0 1 14,0 L 14,14 Z" />
|
||||
<rect id="top" x="20" y="0" width="8" height="14" />
|
||||
<path id="topright" d="M 34,0 A 14,14 0 0 1 48,14 L 34,14 Z" />
|
||||
|
||||
<rect id="left" x="0" y="20" width="14" height="8" />
|
||||
<rect id="center" x="20" y="20" width="8" height="8" />
|
||||
<rect id="right" x="34" y="20" width="14" height="8" />
|
||||
|
||||
<path id="bottomleft" d="M 14,48 A 14,14 0 0 1 0,34 L 14,34 Z" />
|
||||
<rect id="bottom" x="20" y="34" width="8" height="14" />
|
||||
<path id="bottomright" d="M 48,34 A 14,14 0 0 1 34,48 L 34,34 Z" />
|
||||
</g>
|
||||
</svg>
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Name": "Ekklesia"
|
||||
}
|
||||
],
|
||||
"Description": "Cards for a room read from across it",
|
||||
"Id": "ekklesia",
|
||||
"License": "GPLv2+",
|
||||
"Name": "Ekklesia Classroom",
|
||||
"Website": "https://riverofglorymn.org"
|
||||
},
|
||||
"X-Plasma-API": "5.0"
|
||||
}
|
||||
@@ -15,7 +15,7 @@ AccentColor=79,179,168
|
||||
Theme=breeze-dark
|
||||
|
||||
[plasmarc][Theme]
|
||||
name=default
|
||||
name=ekklesia
|
||||
|
||||
[kcminputrc][Mouse]
|
||||
cursorTheme=breeze_cursors
|
||||
|
||||
@@ -123,7 +123,23 @@ rightGap.writeConfig("expanding", true);
|
||||
// about. Naming the four the classroom actually uses is both smaller and
|
||||
// deterministic, and it leaves nothing for a teacher to expand.
|
||||
panel.addWidget("org.kde.plasma.networkmanagement");
|
||||
panel.addWidget("org.kde.plasma.volume");
|
||||
|
||||
var volume = panel.addWidget("org.kde.plasma.volume");
|
||||
volume.currentConfigGroup = ["General"];
|
||||
// No chime when the volume changes. A classroom is often listening to
|
||||
// something when somebody adjusts it, and a beep over a lesson is worse
|
||||
// than no feedback at all — the on-screen display is the feedback, and it
|
||||
// is in the look and feel package where a television can read it.
|
||||
//
|
||||
// Plasma 6 moved this setting out of the applet and into a global one,
|
||||
// and the applet migrates a false written here into it on first run. So
|
||||
// this is both the setting and the instruction to carry it across.
|
||||
volume.writeConfig("volumeFeedback", false);
|
||||
volume.writeConfig("volumeOsd", true);
|
||||
volume.writeConfig("muteOsd", true);
|
||||
volume.writeConfig("outputChangeOsd", true);
|
||||
// A teacher pressing the volume key expects a step they can hear.
|
||||
volume.writeConfig("volumeStep", 5);
|
||||
|
||||
panel.addWidget("org.kde.plasma.notifications");
|
||||
|
||||
@@ -134,8 +150,13 @@ clock.writeConfig("dateFormat", "custom");
|
||||
clock.writeConfig("customDateFormat", "ddd d MMM");
|
||||
clock.writeConfig("showSeconds", 0);
|
||||
clock.writeConfig("dateDisplayFormat", "BelowTime");
|
||||
// Left to itself the clock grows to the height of the bar and becomes the
|
||||
// largest text in the room. It is status, not a headline.
|
||||
clock.writeConfig("autoFontAndSize", false);
|
||||
clock.writeConfig("fontSize", 15);
|
||||
// Sized by the bar, not by a number written here.
|
||||
//
|
||||
// It used to be pinned at 15 points to stop it becoming the largest text
|
||||
// in the room. On a 4K television that was a clock a teacher could not
|
||||
// read from the back of the room, and it was the one thing in the
|
||||
// classroom that did not follow UI_SCALE, because a point size written
|
||||
// into a layout is the same point size at every scale. Plasma's own
|
||||
// sizing follows the bar, and the bar follows the scale.
|
||||
clock.writeConfig("autoFontAndSize", true);
|
||||
clock.writeConfig("fontWeight", 500);
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
# Small cards, top right, out of the way of the bottom bar and of anything
|
||||
# a teacher is reading. Plasma's own notification service does this; the
|
||||
# classroom does not run a notification daemon of its own.
|
||||
# Top right, out of the way of the bottom bar and of anything a teacher is
|
||||
# reading. Plasma's own notification service does this; the classroom does
|
||||
# not run a notification daemon of its own, and replacing the daemon was
|
||||
# never on the table — what the room changes is how the card looks, in the
|
||||
# Plasma style, and how large it is, which follows UI_SCALE like the rest
|
||||
# of the session.
|
||||
[Notifications][$i]
|
||||
PopupPosition=TopRight
|
||||
PopupTimeout=6000
|
||||
# Eight seconds. Six is comfortable at a desk and short at the back of a
|
||||
# classroom, where the card has to be noticed before it can be read.
|
||||
# Critical notifications ignore this and stay until they are dismissed,
|
||||
# which is Plasma's behaviour and the reason it is not changed here.
|
||||
PopupTimeout=8000
|
||||
LowPriorityPopups=false
|
||||
LowPriorityHistory=false
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
[General]
|
||||
AudioFeedback[$i]=false
|
||||
RaiseMaximumVolume[$i]=false
|
||||
VolumeOsd=true
|
||||
MuteOsd=true
|
||||
@@ -0,0 +1,12 @@
|
||||
# Which Plasma style draws the room's popups.
|
||||
#
|
||||
# The style is a set of pictures — see
|
||||
# /usr/share/plasma/desktoptheme/ekklesia — and it is what gives a
|
||||
# notification and the volume display a rounded card instead of Breeze's.
|
||||
# Anything the classroom's style does not provide comes from Breeze, so
|
||||
# this cannot take a popup away, only change how it looks.
|
||||
#
|
||||
# Immutable on the key rather than the group, because the rest of this
|
||||
# file is Plasma's own working state.
|
||||
[Theme]
|
||||
name[$i]=ekklesia
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
|
||||
<policyconfig>
|
||||
<action id="org.ekklesia.classroom.display">
|
||||
<description>Change the classroom display size</description>
|
||||
<message>Enter an administrator password to save this room's display size.</message>
|
||||
<defaults>
|
||||
<allow_any>no</allow_any>
|
||||
<allow_inactive>no</allow_inactive>
|
||||
<allow_active>auth_admin</allow_active>
|
||||
</defaults>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/local/bin/classroom-set-scale</annotate>
|
||||
</action>
|
||||
</policyconfig>
|
||||
@@ -0,0 +1,12 @@
|
||||
# A drive plugged in later.
|
||||
#
|
||||
# The panel's disc player follows the machine's hardware rather than a
|
||||
# setting, so a USB drive connected after the room was set up has to be
|
||||
# noticed. This re-runs the same detection the boot service runs; the
|
||||
# button appears at the next login, because the panel is built at login
|
||||
# and rewriting a running one destroys it — measured.
|
||||
#
|
||||
# --no-block so udev never waits for systemd, and a match narrow enough
|
||||
# that nothing but an optical drive can trigger it.
|
||||
ACTION=="add|remove", SUBSYSTEM=="block", ENV{ID_CDROM}=="1", \
|
||||
RUN+="/usr/bin/systemctl start --no-block ekklesia-classroom-layout.service"
|
||||
|
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 1.8 MiB |
@@ -1096,6 +1096,111 @@ 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.
|
||||
|
||||
## The rooms themselves: 0.1.48
|
||||
|
||||
The first real classroom, Little Waders, took the Plasma build and then
|
||||
reported what a lab cannot: a disc player on a machine with no drive, a
|
||||
clock nobody could read, a browser at half the size of the panel around
|
||||
it, and Parent Call with no broker to call.
|
||||
|
||||
### The broker every room shares
|
||||
|
||||
There is one, at 10.40.0.2, and every classroom uses it. It is a default
|
||||
now rather than something to fill in room by room: a fresh machine is
|
||||
pointed at it, and `classroom-migrate` brings an existing one to the same
|
||||
place — switched on if it was off, addressed if it was blank, and left
|
||||
exactly as it is if somebody deliberately pointed it elsewhere. The room
|
||||
name, the username and the password are read by none of it and printed by
|
||||
none of it, which `tests/test-classroom-migrate-config.sh` checks against
|
||||
the real migration code rather than a copy of it.
|
||||
|
||||
The link restarts on `always` rather than `on-failure`, because it exits
|
||||
cleanly when it finds no broker configured and a machine set up before
|
||||
the church had one would otherwise never pick it up.
|
||||
|
||||
Parent Call now says which of the two problems a teacher has. "Cannot
|
||||
reach the church network" and "This room has no church network set up"
|
||||
send somebody to look at completely different things, and saying the
|
||||
first when the truth is the second costs an hour at a router.
|
||||
|
||||
### A disc player on a machine with no drive
|
||||
|
||||
The detection was right and it ran too late. `classroom-provision` runs at
|
||||
boot *alongside* the session rather than before it, and the panel is built
|
||||
from the layout the moment a session starts: a room told about its own
|
||||
hardware afterwards keeps the wrong bar until the next login.
|
||||
`ekklesia-classroom-layout.service` runs it before the display manager
|
||||
instead, a udev rule re-runs it when a drive is plugged in or pulled out,
|
||||
and the login says so in the journal when the layout and the hardware
|
||||
disagree. Measured in a mount namespace with no `/dev/sr*`: the launcher
|
||||
is removed and the rebuilt panel has ten applets and no disc.
|
||||
|
||||
### One scale, and everything that was not following it
|
||||
|
||||
`UI_SCALE` had been packaged, clamped, documented — and applied by an
|
||||
autostart entry that only appeared in 0.1.47. Three things were still
|
||||
outside it:
|
||||
|
||||
* **The ceiling was 1.5.** A 4K television wants 2.0, so a room asking for
|
||||
it silently got 1.5. It is 3.0 now, and the hidden display dialog
|
||||
offers up to it.
|
||||
* **The clock was pinned at 15 points**, which is the same size at every
|
||||
scale. It is sized by the bar now, which follows the scale like
|
||||
everything else.
|
||||
* **Chrome was not told.** On Wayland it did not follow the compositor's
|
||||
output scale, so the browser was half the size of the panel around it.
|
||||
Both launchers pass `--force-device-scale-factor`, from the one command
|
||||
that owns the number.
|
||||
|
||||
Measured at 200% in the lab: the panel, the clock, the notification card,
|
||||
Chrome's tab strip and the page itself all doubled together.
|
||||
|
||||
### The notification card
|
||||
|
||||
A Plasma style, which is the supported way and the reason no QML was
|
||||
replaced. A style is a set of pictures and anything the classroom does not
|
||||
provide comes from Breeze, so the worst this can do is look wrong — never
|
||||
fail to appear. Replacing the notification plasmoid's QML was the other
|
||||
route and a QML error there is a room with no notifications at all.
|
||||
|
||||
Web pages are blocked from notifying at the policy level, with no prompt:
|
||||
a permission dialog in a classroom is a dialog a child can agree to.
|
||||
|
||||
### The volume display, and a shell package that costs the panel
|
||||
|
||||
The 0.1.47 build replaced Plasma's on-screen volume display with its own,
|
||||
through a shell package of its own. Plasma 6.3 does read the display from
|
||||
there and from nowhere else — the look and feel is not consulted, which
|
||||
was measured — so the mechanism was right.
|
||||
|
||||
**With that shell package selected, plasmashell builds no panel at all.**
|
||||
Not ours, not the stock one. Measured twice, with the 0.1.47 package's own
|
||||
files. The classroom deletes its panel configuration at every login on
|
||||
purpose, so every login was one step away from a television showing a
|
||||
wallpaper and nothing else.
|
||||
|
||||
So it is gone, and the volume display is Plasma's own. It is drawn at the
|
||||
room's scale like everything else, which is what the television actually
|
||||
needed. `classroom-desktop-reset` also strips a `ShellPackage` line out of
|
||||
a profile written while the old build was installed.
|
||||
|
||||
The chime stays off, in `plasmaparc`, which is the file Plasma 6 actually
|
||||
reads for it.
|
||||
|
||||
### What the lab was allowed to reach
|
||||
|
||||
Pointing the package at the real broker had an immediate consequence: the
|
||||
lab connected to it, under a container id, as a room. Nothing was left
|
||||
behind — the broker held no retained status for it afterwards, which was
|
||||
checked — but the lab had no business being there.
|
||||
|
||||
`tools/lab/lab up` now blackholes both church networks before anything
|
||||
starts, and refuses to continue if either is still reachable. The rig had
|
||||
carried a guard for this since it was written and it had never once
|
||||
worked: it used `iptables`, which is not installed in the lab image, and
|
||||
it probed with `/dev/tcp` under `sh`, which is a bash feature and fails
|
||||
under dash whatever the network is doing. Both were reported as success.
|
||||
|
||||
## Unresolved risks
|
||||
|
||||
In the order I would worry about them.
|
||||
@@ -1160,7 +1265,17 @@ In the order I would worry about them.
|
||||
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
|
||||
15. **Nothing in this pass has been on the classroom machines.** The
|
||||
development host has no route to Little Waders — its SSH refuses this
|
||||
machine's key, and the control computer cannot reach it either — so
|
||||
every result above is the lab's. The package is built and the deploy
|
||||
commands are written down; the room itself is unverified.
|
||||
16. **The v2 channel still has no manifest.** `update_classroom_v2.sh`
|
||||
exists now and has not been run. Until it is, a v2 room takes no
|
||||
updates at all, and `classroom-migrate` moves every upgraded machine
|
||||
onto that channel — deliberately, because stable and canary would
|
||||
both hand a Plasma room the sway build back.
|
||||
17. **Ctrl+Alt+T is still registered.** Konsole's own entry declares it
|
||||
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
|
||||
@@ -1260,29 +1375,44 @@ The existing Debian classroom test VM should be kept.
|
||||
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.
|
||||
* **0.1.47's work brought into the repository**, where it had only ever
|
||||
existed in a build directory: the hidden display dialog behind
|
||||
Meta+Ctrl+Alt+Shift+D, its authenticated writer, the polkit action, the
|
||||
autostart entry that applies the scale, and the church's own wallpaper.
|
||||
* **The broker given to every room**, the disc player decided before the
|
||||
session starts, the clock and the browser brought under the one scale,
|
||||
the notification card given a Plasma style of its own, and the shell
|
||||
package that cost the panel taken back out — 0.1.48.
|
||||
|
||||
**Next**, in order:
|
||||
|
||||
0. **The thirty-second Escape test**, before anything is built on the
|
||||
0. **Deploy 0.1.48 to Little Waders and run the list.** Everything in
|
||||
*The rooms themselves* is lab work: the development host cannot reach
|
||||
the classroom machines at all. The deploy and rollback commands are in
|
||||
the report that accompanies this build, and the checks that matter are
|
||||
the panel after a reboot, the clock and the browser at the room's
|
||||
scale, Parent Call with a typed name, the broker, HDMI audio and the
|
||||
volume keys.
|
||||
1. **The thirty-second Escape test**, before anything is built on the
|
||||
gesture: bind a probe entry to `Escape`, press it in a fullscreen
|
||||
video, and see whether the video leaves fullscreen. That settles the
|
||||
one thing this pass could not measure.
|
||||
1. **On the bench, tonight.** Type a name into Parent Call with a real
|
||||
2. **On the bench, tonight.** Type a name into Parent Call with a real
|
||||
keyboard. Switch a television on after the computer and listen. Play a
|
||||
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 the v2 channel. `classroom-migrate` already moves a migrated
|
||||
3. 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:
|
||||
4. 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
|
||||
5. 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
|
||||
6. 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*.
|
||||
6. Live View, when the rest is quiet.
|
||||
7. Live View, when the rest is quiet.
|
||||
|
||||
@@ -87,6 +87,20 @@ container lab. See [../classroom-v2-experiment.md](../classroom-v2-experiment.md
|
||||
| `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. |
|
||||
|
||||
## The rooms themselves
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| `v2-from-package` | the classroom with the church's own wallpaper and the real Wonder Ink mark |
|
||||
| `wonderink-icon` | the same mark as a running application's button |
|
||||
| `display-settings` | the hidden display-size dialog, saying that saving needs an administrator |
|
||||
| `scale-200`, `scale-200-notification`, `scale-200-chrome` | the room at 200%: the panel, a notification card, and the browser, all doubled together |
|
||||
| `notification-style` | the notification card on the classroom's own Plasma style |
|
||||
| `osd-volume` | Plasma's volume display, which the room keeps |
|
||||
| `osd-shellpackage` | the custom one, which rendered correctly and cost the panel |
|
||||
| `pc-unreachable`, `pc-not-configured` | Parent Call telling a broker it cannot reach from one that was never set up |
|
||||
| `dvd-absent-bar` | the bar on a machine with no optical drive |
|
||||
|
||||
## Failures, kept
|
||||
|
||||
| | |
|
||||
|
||||
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 823 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 96 KiB |
@@ -1,106 +1,69 @@
|
||||
# UI scale
|
||||
# Classroom display size (Plasma v2)
|
||||
|
||||
Every classroom gets the same design at a size that suits its TV. A 32"
|
||||
panel a few feet from the children and a 65" set across a hall want
|
||||
different sizes of the same room, not different rooms.
|
||||
Each room keeps its own `UI_SCALE` in `/etc/ekklesia-classroom/device.conf`.
|
||||
Package upgrades preserve the value. Plasma applies it at every classroom login.
|
||||
Native display resolution is unchanged.
|
||||
|
||||
## Changing it
|
||||
## Administrator setting
|
||||
|
||||
Edit `/etc/ekklesia-classroom/device.conf` on that endpoint:
|
||||
On the classroom keyboard press **Meta+Ctrl+Alt+Shift+D** to open Display size.
|
||||
It is hidden from application lists and the teacher's panel. Saving requires
|
||||
administrator authentication through polkit; the keyboard shortcut grants no
|
||||
privilege. Choose a percentage and press Save and apply. Canceling the password
|
||||
prompt leaves the saved setting unchanged.
|
||||
|
||||
Start at **200% for a 4K TV**, which gives 1920×1080 logical space while retaining
|
||||
3840×2160 output. The offered values are 100, 125, 150, 175, 200, 225, 250 and 300%.
|
||||
The choice applies to all connected, enabled displays on that classroom computer.
|
||||
|
||||
For SSH administration, the fixed-purpose helper can also be run as root:
|
||||
|
||||
```
|
||||
UI_SCALE=1.25
|
||||
sudo /usr/local/bin/classroom-set-scale 2
|
||||
```
|
||||
|
||||
Then restart the classroom shell:
|
||||
It writes only UI_SCALE, preserving room configuration, broker credentials and
|
||||
file ownership and permissions. The new size takes effect at the next classroom
|
||||
login, or by running `classroom-scale` inside the classroom's graphical session.
|
||||
The hidden window saves and applies in one operation.
|
||||
|
||||
```
|
||||
sudo systemctl restart getty@tty1
|
||||
```
|
||||
## Implementation and validation limits
|
||||
|
||||
That ends the session and the autologin brings it straight back at the
|
||||
new size. A reboot does the same. Nothing else needs touching, and no
|
||||
CSS or source file is ever edited to change a room's size.
|
||||
`classroom-scale` uses `kscreen-doctor`; the login autostart runs only in KDE and
|
||||
the script applies only to the classroom account. Values read from older/manual
|
||||
configurations remain clamped to 0.8–3.0; invalid/nonfinite values become 1.0.
|
||||
The helper accepts only the window's predefined choices, takes no paths, and
|
||||
writes the root-owned settings file atomically. Polkit requires auth_admin for
|
||||
every save, with no retained authorization.
|
||||
|
||||
## What it scales
|
||||
Hardware checks required: shortcut registration, administrator authentication,
|
||||
200% at 4K, logout/reboot persistence, and Chrome/Parent Call/DVD sizing. XWayland
|
||||
applications may be softer than native Wayland applications when scaled.
|
||||
|
||||
**Everything.** UI_SCALE is a property of the television, not of one
|
||||
program, so it is applied where it reaches the whole session: sway's
|
||||
output scale.
|
||||
## Wallpaper in the v2 package
|
||||
|
||||
- Chrome's tabs, omnibox, toolbar icons and menus
|
||||
- VLC, and any other application's own interface
|
||||
- The dock, the clock, notifications and the DVD control bar
|
||||
- Mouse hit targets, because they scale with the widgets
|
||||
The approved background source is `config/wallpapers/classroom-calm.png`. The
|
||||
package installs it at `/usr/share/backgrounds/ekklesia/classroom-calm.png`, the
|
||||
path used by the packaged Plasma layout. The layout is rebuilt at login, so
|
||||
upgraded classrooms receive the package wallpaper without per-room copying.
|
||||
|
||||
The display mode is **not** changed. The panel stays at its native
|
||||
1920x1080. Only the logical coordinate space shrinks, which is what
|
||||
makes everything drawn in it larger:
|
||||
## What the one number reaches
|
||||
|
||||
| UI_SCALE | Logical space | Roughly |
|
||||
| --- | --- | --- |
|
||||
| 0.8 | 2400x1350 | Small or close-up panel |
|
||||
| 1.0 | 1920x1080 | Default. 32–50" classroom TV |
|
||||
| 1.25 | 1536x864 | Large room, or a set viewed from further back |
|
||||
| 1.5 | 1280x720 | 65"+ |
|
||||
`classroom-ui-scale` prints it, clamped, and everything that cannot read
|
||||
the display scale for itself asks that command rather than the file:
|
||||
|
||||
Anything outside 0.8–1.5 is clamped, and anything that is not a number
|
||||
falls back to 1.0, so a typo in device.conf cannot produce an unusable
|
||||
shell.
|
||||
* **Plasma** — the panel, the clock, notifications, the volume display and
|
||||
every dialog, because `classroom-scale` sets it as the display's scale
|
||||
through `kscreen-doctor` at every login.
|
||||
* **Chrome and Wonder Ink** — `--force-device-scale-factor`, because
|
||||
Chrome on Wayland was measured not to follow the compositor's output
|
||||
scale on its own. It is the same number, not a second scaling: Chrome
|
||||
replaces its detected scale with it.
|
||||
* **The disc player's own surfaces** — they are measured in the logical
|
||||
pixels the compositor is already scaling, so they follow without being
|
||||
told.
|
||||
|
||||
## Measured
|
||||
|
||||
Against 1920x1080 under sway, with Chrome 152:
|
||||
|
||||
| | 1.0 | 1.25 | 1.5 |
|
||||
| --- | --- | --- | --- |
|
||||
| Chrome tab strip | 40px | 49px (x1.23) | 59px (x1.48) |
|
||||
| sway title bar | 25px | 31px (x1.24) | 38px (x1.52) |
|
||||
| Classroom dock | 74px | 92px (x1.24) | 110px (x1.49) |
|
||||
|
||||
Everything scales once, together.
|
||||
|
||||
## Why our own surfaces do not scale themselves
|
||||
|
||||
`classroom-scale` runs first in the sway config and sets the output
|
||||
scale. From then on the dock, the notifications and the DVD control bar
|
||||
are measured in the same logical pixels sway is already scaling, so they
|
||||
must render **unscaled** — `SHELL_SCALE` in `classroom_config.py` is
|
||||
`1.0`. Scaling them again would apply UI_SCALE twice and the dock would
|
||||
come out half again too big.
|
||||
|
||||
The stylesheet templates still take a scale, and `SHELL_SCALE` is the
|
||||
one place it comes from. If compositor scaling ever has to be abandoned,
|
||||
setting it back to `ui_scale(config)` restores per-surface scaling
|
||||
without touching a template.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
**GTK 3 has no fractional scaling.** At UI_SCALE=1.5 sway advertises an
|
||||
integer scale of 2 to GTK, which renders at 2x, and sway scales the
|
||||
result down to 1.5x. The dock is the right size but very slightly softer
|
||||
than at 1.0. On a TV at classroom distance this is not visible.
|
||||
|
||||
Icons have to be drawn for that themselves. `new_from_file_at_scale`
|
||||
rasterises an SVG at its *logical* size, and the compositor then
|
||||
enlarges that finished raster — which made icons visibly soft on a 24"
|
||||
monitor while the clock text beside them stayed sharp. `classroom_icons`
|
||||
renders each SVG at `size x scale_factor` physical pixels and hands GTK
|
||||
a cairo surface carrying that device scale, so the artwork lands at the
|
||||
display's real density and the layout is unchanged. Measured on the
|
||||
Chrome icon, peak edge contrast at UI_SCALE 1.5 went from 49 to 83 out
|
||||
of 255, and at 2.0 from 37 to 82; at 1.0 it is identical to before.
|
||||
|
||||
**XWayland is softer at fractional scale.** X11 clients have no concept
|
||||
of a fractional output scale, so sway renders them at the logical size
|
||||
and scales the buffer. Measured on terminal text, scale 1.5 produced
|
||||
about 50% more intermediate-tone pixels than scale 1.0 — real, modest
|
||||
softening.
|
||||
|
||||
VLC is the only XWayland application in the classroom, and in DVD mode
|
||||
none of its interface is on screen: only video, from a 720x480 disc
|
||||
being upscaled regardless. Chrome runs on Wayland natively and is sharp
|
||||
at every scale, which is what matters for reading.
|
||||
|
||||
At UI_SCALE=1.0 nothing is scaled at all and the session is identical to
|
||||
one with no scaling.
|
||||
The clock is the one thing that used to be left behind. It was pinned at
|
||||
15 points, which is the same 15 points at every scale, and on a 4K
|
||||
television it was a clock nobody at the back of the room could read. It
|
||||
is sized by the bar now.
|
||||
|
||||
@@ -12,11 +12,15 @@ CONFIG_FILE = Path("/etc/ekklesia-classroom/device.conf")
|
||||
|
||||
TRUTHY = {"1", "true", "yes", "on", "enabled"}
|
||||
|
||||
# The dock is tuned for a 1080p classroom TV at 1.0. Smaller rooms with a
|
||||
# 32" panel sit near the bottom of this range, a 65" set near the top.
|
||||
# Tuned for a 1080p classroom television at 1.0. A 4K set wants 2.0,
|
||||
# which is the same interface at the same physical size on a display with
|
||||
# twice the pixels.
|
||||
UI_SCALE_DEFAULT = 1.0
|
||||
UI_SCALE_MIN = 0.8
|
||||
UI_SCALE_MAX = 1.5
|
||||
# 2.0 is a 4K television drawn at 1080p sizes, which is the setting the
|
||||
# large classroom screens actually need. The old ceiling of 1.5 could not
|
||||
# express it, so a room asking for 2.0 silently got 1.5.
|
||||
UI_SCALE_MAX = 3.0
|
||||
|
||||
|
||||
DEVICE_CONFIG_TEMPLATE = """# Ekklesia Classroom device configuration
|
||||
@@ -40,14 +44,22 @@ LIVE_ENABLED=false
|
||||
# and sound that is already playing is moved across to it.
|
||||
AUDIO_OUTPUT=auto
|
||||
|
||||
# Size of the dock, clock and notifications, 0.8 to 1.5.
|
||||
# Larger rooms and larger TVs want a larger number.
|
||||
# How large everything in the room is drawn: the panel, the clock, the
|
||||
# browser, Wonder Ink, the disc player, notifications and the volume
|
||||
# display, all from this one number. 1.0 to 3.0.
|
||||
#
|
||||
# 1.0 suits a 1080p television at ordinary classroom distance. 2.0 is
|
||||
# right for a 4K set, where everything is otherwise half the size it
|
||||
# looks. This is an administrator's setting: there is no way to reach it
|
||||
# from the room, on purpose.
|
||||
UI_SCALE=1.0
|
||||
|
||||
# Church message broker. The room is this machine's hostname unless ROOM
|
||||
# is set. Leave MQTT_ENABLED=false until a broker exists.
|
||||
MQTT_ENABLED=false
|
||||
MQTT_HOST=
|
||||
# Church message broker. Every classroom uses the same one, so the address
|
||||
# is a default rather than something to fill in room by room. The room is
|
||||
# this machine's hostname unless ROOM is set. A username and password are
|
||||
# only needed if the broker asks for them.
|
||||
MQTT_ENABLED=true
|
||||
MQTT_HOST=10.40.0.2
|
||||
MQTT_PORT=1883
|
||||
MQTT_USERNAME=
|
||||
MQTT_PASSWORD=
|
||||
@@ -82,17 +94,25 @@ AUDIO_OUTPUT=auto
|
||||
),
|
||||
(
|
||||
"UI_SCALE",
|
||||
"""# Size of the dock, clock and notifications, 0.8 to 1.5.
|
||||
# Larger rooms and larger TVs want a larger number.
|
||||
"""# How large everything in the room is drawn: the panel, the clock, the
|
||||
# browser, Wonder Ink, the disc player, notifications and the volume
|
||||
# display, all from this one number. 1.0 to 3.0.
|
||||
#
|
||||
# 1.0 suits a 1080p television at ordinary classroom distance. 2.0 is
|
||||
# right for a 4K set, where everything is otherwise half the size it
|
||||
# looks. This is an administrator's setting: there is no way to reach it
|
||||
# from the room, on purpose.
|
||||
UI_SCALE=1.0
|
||||
""",
|
||||
),
|
||||
(
|
||||
"MQTT_ENABLED",
|
||||
"""# Church message broker. The room is this machine's hostname unless ROOM
|
||||
# is set. Leave MQTT_ENABLED=false until a broker exists.
|
||||
MQTT_ENABLED=false
|
||||
MQTT_HOST=
|
||||
"""# Church message broker. Every classroom uses the same one, so the address
|
||||
# is a default rather than something to fill in room by room. The room is
|
||||
# this machine's hostname unless ROOM is set. A username and password are
|
||||
# only needed if the broker asks for them.
|
||||
MQTT_ENABLED=true
|
||||
MQTT_HOST=10.40.0.2
|
||||
MQTT_PORT=1883
|
||||
MQTT_USERNAME=
|
||||
MQTT_PASSWORD=
|
||||
|
||||
@@ -268,7 +268,26 @@ def read_status(payload, request_id=None):
|
||||
}
|
||||
|
||||
|
||||
def availability(mode, coordinator_online, broker_reachable=True):
|
||||
# Why the room has no broker, in the teacher's words.
|
||||
#
|
||||
# "Cannot reach" and "is not set up" are different problems with different
|
||||
# people to fetch: the first is a network or a broker that is down and
|
||||
# will come back, the second is a machine that was never told where the
|
||||
# church network is and needs an administrator. Saying the first when the
|
||||
# truth is the second sends somebody to look at a router for an hour.
|
||||
BROKER_UNREACHABLE = "Cannot reach the church network"
|
||||
BROKER_WORDING = {
|
||||
"not configured": "This room has no church network set up",
|
||||
"no broker library": "This room has no church network set up",
|
||||
}
|
||||
|
||||
|
||||
def broker_message(why=None):
|
||||
"""What to tell the teacher about a broker that is not there."""
|
||||
return BROKER_WORDING.get(why, BROKER_UNREACHABLE)
|
||||
|
||||
|
||||
def availability(mode, coordinator_online, broker_reachable=True, why=None):
|
||||
"""Whether a teacher may send a call now, and what to say if not.
|
||||
|
||||
Returns (can_send, message). The wording for an off mode is the
|
||||
@@ -276,7 +295,7 @@ def availability(mode, coordinator_online, broker_reachable=True):
|
||||
would.
|
||||
"""
|
||||
if not broker_reachable:
|
||||
return False, "Cannot reach the church network"
|
||||
return False, broker_message(why)
|
||||
|
||||
if not coordinator_online:
|
||||
return False, "The sanctuary system is not running"
|
||||
|
||||
@@ -4,7 +4,7 @@ Section: misc
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
Maintainer: Ekklesia
|
||||
Depends: plasma-desktop, plasma-workspace, kwin-wayland, plasma-nm, plasma-pa, plasma-widgets-addons, konsole, sddm, dbus-user-session, xwayland, fonts-dejavu, fonts-noto, network-manager, pipewire, pipewire-audio, wireplumber, pulseaudio-utils, cups, cups-client, cups-filters, foomatic-db-compressed-ppds, python3-gi, python3-gi-cairo, gir1.2-gtk-3.0, gir1.2-gtklayershell-0.1, libnotify-bin, curl, gnupg, ca-certificates, python3-paho-mqtt
|
||||
Depends: pkexec, polkit-kde-agent-1, plasma-desktop, plasma-workspace, kwin-wayland, plasma-nm, plasma-pa, plasma-widgets-addons, konsole, sddm, dbus-user-session, xwayland, fonts-dejavu, fonts-noto, network-manager, pipewire, pipewire-audio, wireplumber, pulseaudio-utils, cups, cups-client, cups-filters, foomatic-db-compressed-ppds, python3-gi, python3-gi-cairo, gir1.2-gtk-3.0, gir1.2-gtklayershell-0.1, libnotify-bin, curl, gnupg, ca-certificates, python3-paho-mqtt
|
||||
Recommends: vlc
|
||||
Conflicts: plasma-welcome, plasma-discover, kdeconnect, kup-backup
|
||||
Description: Ekklesia managed classroom environment
|
||||
|
||||
@@ -55,11 +55,21 @@ systemctl reload systemd-logind >/dev/null 2>&1 || true
|
||||
# Autologin into the classroom, and back into it if the session ever ends.
|
||||
systemctl enable sddm.service >/dev/null 2>&1 || true
|
||||
|
||||
# The room's own user services: the audio policy, and the drop-in that stops
|
||||
# Plasma giving up on a repeatedly crashing panel.
|
||||
# The room's own user services: the audio policy, and the drop-in that
|
||||
# stops Plasma giving up on a repeatedly crashing panel. The display scale
|
||||
# is applied by an XDG autostart entry instead, which is already how it
|
||||
# reaches a session on the classroom machines.
|
||||
systemctl --global enable ekklesia-classroom-audio.service >/dev/null 2>&1 || true
|
||||
systemctl --global enable ekklesia-classroom-desktop.service >/dev/null 2>&1 || true
|
||||
|
||||
# The panel's disc player is decided before the display manager starts, not
|
||||
# at the same time as it. Enabled rather than started: this install has
|
||||
# just run the same command directly, below.
|
||||
systemctl enable ekklesia-classroom-layout.service >/dev/null 2>&1 || true
|
||||
|
||||
# So a drive plugged in later is noticed without a reboot.
|
||||
udevadm control --reload-rules >/dev/null 2>&1 || true
|
||||
|
||||
# Match the bar to this machine now, so the first login already has the
|
||||
# right one rather than waiting for the next provisioning run.
|
||||
/usr/local/bin/classroom-desktop-layout >/dev/null 2>&1 || true
|
||||
|
||||
@@ -24,8 +24,17 @@ set -euo pipefail
|
||||
# exists to prevent.
|
||||
HOME_PAGE='https://www.youtube.com/'
|
||||
|
||||
# One number for the whole room. Chrome on Wayland was measured not to
|
||||
# follow the compositor's output scale on its own, so the 4K television
|
||||
# got a browser at half the size of everything around it. Forcing the
|
||||
# same number the display is scaled by is a statement of the same fact,
|
||||
# not a second scaling: Chrome replaces its detected scale with this
|
||||
# rather than multiplying by it.
|
||||
SCALE="$(/usr/local/bin/classroom-ui-scale 2>/dev/null || echo 1)"
|
||||
|
||||
exec google-chrome \
|
||||
--ozone-platform=wayland \
|
||||
--force-device-scale-factor="$SCALE" \
|
||||
--start-maximized \
|
||||
--no-first-run \
|
||||
--no-default-browser-check \
|
||||
|
||||
@@ -6,6 +6,13 @@ There is exactly one thing about the bar that depends on hardware: a room
|
||||
with no optical drive should not be offered a disc player. This edits the
|
||||
one marked line in the installed layout, and nothing else.
|
||||
|
||||
Run before the display manager by ekklesia-classroom-layout.service, and
|
||||
again by udev when a drive is plugged in or pulled out. That ordering is
|
||||
the whole point: the panel is built from this layout the moment a session
|
||||
starts, and a room that is told about its hardware afterwards keeps the
|
||||
wrong bar until the next login. It was being left to the provisioning
|
||||
service, which runs at boot alongside the session rather than before it.
|
||||
|
||||
It writes the *layout*, not the running panel. Editing
|
||||
plasma-org.kde.plasma.desktop-appletsrc underneath a live plasmashell was
|
||||
tried and it destroyed the panel: the shell owns that file and rewrites
|
||||
@@ -30,12 +37,29 @@ LAYOUT = (
|
||||
|
||||
MARKER = "/* CLASSROOM-DVD-LAUNCHER */"
|
||||
|
||||
# The kernel names optical drives sr0, sr1, ... regardless of bus.
|
||||
# The kernel names optical drives sr0, sr1, ... regardless of bus. Both
|
||||
# views are checked: /dev is what anything opening the drive will use, and
|
||||
# /sys/block is the kernel's own list, which cannot be left behind by a
|
||||
# device node somebody or something created by hand.
|
||||
DRIVE_GLOB = "/dev/sr[0-9]*"
|
||||
SYSFS_GLOB = "/sys/block/sr[0-9]*"
|
||||
|
||||
|
||||
def drives():
|
||||
return sorted(glob.glob(DRIVE_GLOB)), sorted(glob.glob(SYSFS_GLOB))
|
||||
|
||||
|
||||
def has_optical_drive():
|
||||
return bool(glob.glob(DRIVE_GLOB))
|
||||
nodes, blocks = drives()
|
||||
|
||||
# Said out loud every time. When a room shows a disc player it has no
|
||||
# drive for, or hides one it has, this line in the journal is the
|
||||
# difference between an answer and an afternoon.
|
||||
print("classroom-desktop-layout: /dev "
|
||||
f"[{', '.join(nodes) or 'none'}], /sys/block "
|
||||
f"[{', '.join(blocks) or 'none'}]")
|
||||
|
||||
return bool(nodes and blocks)
|
||||
|
||||
|
||||
def rewrite(text, wanted):
|
||||
|
||||
@@ -133,6 +133,36 @@ if [[ -f "$CONFIG_HOME/powermanagementprofilesrc" ]]; then
|
||||
say "cleared the user's power settings; the package supplies them"
|
||||
fi
|
||||
|
||||
# THE DISC PLAYER, AND WHETHER THIS MACHINE HAS ONE.
|
||||
#
|
||||
# The panel about to be built comes from the packaged layout, and the one
|
||||
# line in it that depends on hardware is written by
|
||||
# ekklesia-classroom-layout.service before the display manager starts.
|
||||
# This cannot write /usr and does not try. It checks, because a room
|
||||
# showing a disc player it has no drive for is a teacher clicking a button
|
||||
# that can only disappoint, and the journal should say so at the moment
|
||||
# the panel is built rather than after somebody notices.
|
||||
LAYOUT=/usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/layouts/org.kde.plasma.desktop-layout.js
|
||||
|
||||
if [[ -r "$LAYOUT" ]]; then
|
||||
if compgen -G "/dev/sr[0-9]*" >/dev/null; then
|
||||
drive=yes
|
||||
else
|
||||
drive=no
|
||||
fi
|
||||
|
||||
if grep -q '^\s*/\* CLASSROOM-DVD-LAUNCHER \*/' "$LAYOUT"; then
|
||||
offered=yes
|
||||
else
|
||||
offered=no
|
||||
fi
|
||||
|
||||
if [[ "$drive" != "$offered" ]]; then
|
||||
say "WARNING: this machine has drive=$drive but the layout offers"
|
||||
say " the disc player=$offered — run classroom-desktop-layout"
|
||||
fi
|
||||
fi
|
||||
|
||||
CONFIG="$CONFIG_HOME/plasma-org.kde.plasma.desktop-appletsrc"
|
||||
|
||||
if [[ -e "$CONFIG" ]]; then
|
||||
@@ -142,6 +172,28 @@ else
|
||||
say "no panel configuration to clear"
|
||||
fi
|
||||
|
||||
# A shell package the classroom used to select.
|
||||
#
|
||||
# One build chose a shell package of its own so that it could replace the
|
||||
# volume display. With one selected, plasmashell builds no panel at all —
|
||||
# and this script deletes the panel at every login, so the room came up
|
||||
# with an empty screen. The package no longer ships it; this removes the
|
||||
# choice from a profile that was written while it did.
|
||||
if [[ -f "$CONFIG_HOME/plasmashellrc" ]] \
|
||||
&& grep -q "^ShellPackage=" "$CONFIG_HOME/plasmashellrc" 2>/dev/null; then
|
||||
sed -i "/^ShellPackage=/d" "$CONFIG_HOME/plasmashellrc"
|
||||
say "removed a shell package choice that leaves the room with no panel"
|
||||
fi
|
||||
|
||||
# The display, like the panel, comes from the package rather than from
|
||||
# whatever the room last negotiated. KWin remembers a resolution and a
|
||||
# scale per output in this file, and a television that once came up in the
|
||||
# wrong mode keeps it for good — the scale service applies UI_SCALE again
|
||||
# a moment after this, so there is nothing here to preserve. Seen in the
|
||||
# lab: an output that had been through two scale changes stayed at a
|
||||
# resolution nothing on the machine had asked for.
|
||||
rm -f "$CONFIG_HOME/kwinoutputconfig.json" 2>/dev/null || true
|
||||
|
||||
# The icon cache remembers a name it once failed to find, which outlives
|
||||
# the package that later supplies it.
|
||||
rm -f "$CACHE_HOME/icon-cache.kcache" 2>/dev/null || true
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/python3
|
||||
"""Hidden maintenance UI. Saving requires an administrator's password."""
|
||||
import os
|
||||
import pwd
|
||||
import subprocess
|
||||
import sys
|
||||
import gi
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk
|
||||
sys.path.insert(0, "/usr/local/lib/ekklesia-classroom")
|
||||
from classroom_config import read_device_config, ui_scale
|
||||
|
||||
def main():
|
||||
if pwd.getpwuid(os.getuid()).pw_name != "classroom":
|
||||
sys.exit("Open this window from the classroom display session.")
|
||||
window = Gtk.Dialog(title="Classroom display size")
|
||||
window.set_default_size(440, 180)
|
||||
window.add_button("Close", Gtk.ResponseType.CANCEL)
|
||||
window.add_button("Save and apply", Gtk.ResponseType.OK)
|
||||
box = window.get_content_area()
|
||||
box.set_border_width(20)
|
||||
box.set_spacing(16)
|
||||
label = Gtk.Label(label="Display size for this room\n200% is a good starting point for a 4K TV.")
|
||||
box.add(label)
|
||||
sizes = Gtk.ComboBoxText()
|
||||
current = ui_scale(read_device_config())
|
||||
choices = [1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 3.0]
|
||||
if current not in choices:
|
||||
choices.append(current)
|
||||
choices.sort()
|
||||
for value in choices:
|
||||
sizes.append(str(value), f"{value * 100:g}%")
|
||||
sizes.set_active_id(str(current))
|
||||
box.add(sizes)
|
||||
box.add(Gtk.Label(label="Saving requires an administrator password."))
|
||||
window.show_all()
|
||||
while window.run() == Gtk.ResponseType.OK:
|
||||
selected = sizes.get_active_id()
|
||||
if float(selected) == current:
|
||||
continue
|
||||
result = subprocess.run(["pkexec", "/usr/local/bin/classroom-set-scale", selected], capture_output=True, text=True)
|
||||
if result.returncode:
|
||||
message = "No change saved. Authentication was cancelled or the setting could not be saved."
|
||||
else:
|
||||
applied = subprocess.run(["/usr/local/bin/classroom-scale"], capture_output=True, text=True)
|
||||
current = float(selected)
|
||||
message = "Display size saved and applied." if applied.returncode == 0 else "Display size saved, but applying it failed. Sign out and back in to retry."
|
||||
info = Gtk.MessageDialog(transient_for=window, modal=True, message_type=Gtk.MessageType.INFO, buttons=Gtk.ButtonsType.OK, text=message)
|
||||
info.run()
|
||||
info.destroy()
|
||||
window.destroy()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -167,16 +167,32 @@ rmdir --ignore-fail-on-non-empty \
|
||||
# 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.
|
||||
#
|
||||
# The broker is the same for every classroom, so a room that was set up
|
||||
# before there was one — blank address, switched off — is given the
|
||||
# address rather than left with Parent Call saying there is no church
|
||||
# network. A room already pointed somewhere else is left pointed there:
|
||||
# that is a decision somebody made. The username and the password are
|
||||
# never read, never written and never printed.
|
||||
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"}
|
||||
# Every channel that serves a desktop this package replaces. A machine
|
||||
# that has just installed the Plasma classroom must not be left pointed at
|
||||
# a line that will hand it the sway build back at the next release: stable
|
||||
# and canary both would. A room already on the v2 channel, or on one
|
||||
# somebody named deliberately, is left where it is.
|
||||
OBSOLETE_CHANNELS = {"stable", "canary", "hyprland"}
|
||||
CHANNEL = "classroom-v2"
|
||||
|
||||
# The church broker. One address for every room.
|
||||
BROKER_HOST = "10.40.0.2"
|
||||
BROKER_PORT = "1883"
|
||||
TRUTHY = {"1", "true", "yes", "on", "enabled"}
|
||||
|
||||
path = sys.argv[1]
|
||||
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
@@ -184,6 +200,12 @@ with open(path, "r", encoding="utf-8") as handle:
|
||||
|
||||
out = []
|
||||
changed = []
|
||||
seen = set()
|
||||
|
||||
|
||||
def value_of(line):
|
||||
return line.split("=", 1)[1].strip()
|
||||
|
||||
|
||||
for line in lines:
|
||||
key = line.split("=", 1)[0].strip() if "=" in line else ""
|
||||
@@ -192,15 +214,43 @@ for line in lines:
|
||||
changed.append(f"dropped {key}")
|
||||
continue
|
||||
|
||||
if key:
|
||||
seen.add(key)
|
||||
|
||||
if key == "CHANNEL":
|
||||
value = line.split("=", 1)[1].strip()
|
||||
value = value_of(line)
|
||||
|
||||
if value.lower() in OBSOLETE_CHANNELS:
|
||||
changed.append(f"moved off the {value} channel onto {CHANNEL}")
|
||||
line = f"CHANNEL={CHANNEL}\n"
|
||||
|
||||
elif key == "MQTT_ENABLED":
|
||||
if value_of(line).lower() not in TRUTHY:
|
||||
changed.append("switched the broker on")
|
||||
line = "MQTT_ENABLED=true\n"
|
||||
|
||||
elif key == "MQTT_HOST":
|
||||
if not value_of(line):
|
||||
changed.append(f"filled in the broker address {BROKER_HOST}")
|
||||
line = f"MQTT_HOST={BROKER_HOST}\n"
|
||||
|
||||
elif key == "MQTT_PORT":
|
||||
if not value_of(line):
|
||||
changed.append(f"filled in the broker port {BROKER_PORT}")
|
||||
line = f"MQTT_PORT={BROKER_PORT}\n"
|
||||
|
||||
out.append(line)
|
||||
|
||||
# A room old enough to predate a setting has no line to correct.
|
||||
for key, value in (
|
||||
("MQTT_ENABLED", "true"),
|
||||
("MQTT_HOST", BROKER_HOST),
|
||||
("MQTT_PORT", BROKER_PORT),
|
||||
):
|
||||
if key not in seen:
|
||||
changed.append(f"added {key}")
|
||||
out.append(f"{key}={value}\n")
|
||||
|
||||
if changed:
|
||||
with open(path, "w", encoding="utf-8") as handle:
|
||||
handle.writelines(out)
|
||||
|
||||
@@ -541,7 +541,10 @@ class ParentCall(Gtk.Window):
|
||||
self.broker_reachable = data.get("reachable")
|
||||
|
||||
if not self.broker_reachable:
|
||||
self._unavailable("Cannot reach the church network")
|
||||
# Why it is not there decides who the teacher should
|
||||
# fetch, so the reason the broker thread already knows is
|
||||
# carried through to the card rather than flattened.
|
||||
self._unavailable(contract.broker_message(data.get("why")))
|
||||
else:
|
||||
self._after(RETAINED_GRACE, self._retained_deadline)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ is the whole of what replaced the compositor-specific version.
|
||||
|
||||
import json
|
||||
import os
|
||||
import pwd
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
@@ -41,11 +42,13 @@ def outputs():
|
||||
|
||||
return [
|
||||
output["name"] for output in found
|
||||
if output.get("name") and output.get("connected", True)
|
||||
if output.get("name") and output.get("connected", True) and output.get("enabled", True)
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
if pwd.getpwuid(os.getuid()).pw_name != "classroom":
|
||||
return 0
|
||||
# classroom_config clamps a nonsense or out-of-range value rather than
|
||||
# obeying it, so a mistyped UI_SCALE cannot leave a room unusable.
|
||||
scale = ui_scale(read_device_config())
|
||||
@@ -53,8 +56,9 @@ def main():
|
||||
|
||||
if not found:
|
||||
print("classroom-scale: no outputs", file=sys.stderr)
|
||||
return 0
|
||||
return 1
|
||||
|
||||
failed = False
|
||||
for output in found:
|
||||
result = subprocess.run(
|
||||
["kscreen-doctor", f"output.{output}.scale.{scale}"],
|
||||
@@ -64,10 +68,11 @@ def main():
|
||||
if result.returncode == 0:
|
||||
print(f"classroom-scale: {output} scale {scale}")
|
||||
else:
|
||||
failed = True
|
||||
print(f"classroom-scale: {output}: {result.stderr.strip()}",
|
||||
file=sys.stderr)
|
||||
|
||||
return 0
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/python3 -I
|
||||
"""Privileged, fixed-purpose writer; called through an auth_admin polkit action."""
|
||||
import fcntl
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
CHOICES = (1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 3.0)
|
||||
|
||||
def updated(text, value):
|
||||
out = []
|
||||
for line in text.splitlines(keepends=True):
|
||||
if line.split("=", 1)[0].strip() != "UI_SCALE":
|
||||
out.append(line)
|
||||
text = "".join(out)
|
||||
return text + ("\n" if text and not text.endswith("\n") else "") + f"UI_SCALE={value:g}\n"
|
||||
|
||||
def main():
|
||||
if os.geteuid() != 0:
|
||||
sys.exit("Administrator authentication is required.")
|
||||
try:
|
||||
value = float(sys.argv[1]) if len(sys.argv) == 2 else None
|
||||
except ValueError:
|
||||
value = None
|
||||
if value not in CHOICES:
|
||||
sys.exit("Choose one of the supported display sizes.")
|
||||
path = Path("/etc/ekklesia-classroom/device.conf")
|
||||
with open("/run/lock/ekklesia-display-settings.lock", "w") as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX)
|
||||
info = path.stat()
|
||||
text = updated(path.read_text(), value)
|
||||
fd, name = tempfile.mkstemp(prefix=".device-scale-", dir=path.parent)
|
||||
try:
|
||||
with os.fdopen(fd, "w") as handle:
|
||||
os.fchown(handle.fileno(), info.st_uid, info.st_gid)
|
||||
os.fchmod(handle.fileno(), info.st_mode & 0o777)
|
||||
handle.write(text)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
os.replace(name, path)
|
||||
finally:
|
||||
if os.path.exists(name):
|
||||
os.unlink(name)
|
||||
print("Display size saved for this room.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Print the room's UI scale, clamped, as one number.
|
||||
|
||||
There is one scale in a classroom and this is where everything asks for
|
||||
it: the Plasma session applies it to the display, and the browser and
|
||||
Wonder Ink are told it explicitly because Chrome was measured not to
|
||||
follow the compositor on its own. A shell script can read device.conf in
|
||||
one line, but then there are four places that clamp, default and parse a
|
||||
setting differently — which is how a room ends up with a panel at one
|
||||
size and a browser at another.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/usr/local/lib/ekklesia-classroom")
|
||||
sys.path.insert(
|
||||
0,
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "lib"),
|
||||
)
|
||||
|
||||
from classroom_config import read_device_config, ui_scale # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
scale = ui_scale(read_device_config())
|
||||
|
||||
# 2 rather than 2.0: Chrome takes either, and a whole number is what a
|
||||
# person reads in a journal line without wondering what it means.
|
||||
if scale == int(scale):
|
||||
print(int(scale))
|
||||
else:
|
||||
print(f"{scale:g}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -24,6 +24,14 @@ set -euo pipefail
|
||||
# browser.
|
||||
|
||||
HOME_PAGE='https://app.wonderink.org/'
|
||||
|
||||
# One number for the whole room. Chrome on Wayland was measured not to
|
||||
# follow the compositor's output scale on its own, so the 4K television
|
||||
# got a browser at half the size of everything around it. Forcing the
|
||||
# same number the display is scaled by is a statement of the same fact,
|
||||
# not a second scaling: Chrome replaces its detected scale with this
|
||||
# rather than multiplying by it.
|
||||
SCALE="$(/usr/local/bin/classroom-ui-scale 2>/dev/null || echo 1)"
|
||||
PROFILE="${XDG_DATA_HOME:-$HOME/.local/share}/ekklesia-classroom/wonderink"
|
||||
|
||||
mkdir -p "$PROFILE"
|
||||
@@ -32,6 +40,7 @@ exec google-chrome \
|
||||
--user-data-dir="$PROFILE" \
|
||||
--app="$HOME_PAGE" \
|
||||
--ozone-platform=wayland \
|
||||
--force-device-scale-factor="$SCALE" \
|
||||
--start-maximized \
|
||||
--no-first-run \
|
||||
--no-default-browser-check \
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
[Unit]
|
||||
Description=Ekklesia Classroom panel layout for this machine
|
||||
Documentation=file:///usr/local/bin/classroom-desktop-layout
|
||||
# Before the display manager, because the panel is built from the layout
|
||||
# the moment a session starts and there is no second chance until the next
|
||||
# login. This used to be left to the provisioning service, which runs at
|
||||
# boot alongside the session rather than before it: a room with no optical
|
||||
# drive could be shown a disc player anyway, and it stayed there all day.
|
||||
Before=display-manager.service sddm.service
|
||||
# Not ordered after anything that waits for the network. The question is
|
||||
# about hardware in this machine and must never hold a boot up.
|
||||
After=local-fs.target
|
||||
ConditionPathExists=/usr/local/bin/classroom-desktop-layout
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/classroom-desktop-layout
|
||||
RemainAfterExit=no
|
||||
# A layout that cannot be written is not a reason to refuse to start the
|
||||
# classroom. It is a reason to say so in the journal.
|
||||
SuccessExitStatus=0 1
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical.target
|
||||
@@ -14,7 +14,12 @@ Type=simple
|
||||
User=classroom
|
||||
Group=classroom
|
||||
ExecStart=/usr/local/bin/classroom-mqtt
|
||||
Restart=on-failure
|
||||
# always, not on-failure. The link exits cleanly when it finds no broker
|
||||
# configured, and on-failure means a machine that was set up before the
|
||||
# church had a broker would never pick one up until somebody rebooted it.
|
||||
# Reconnection while it is running is paho's own, with a backoff; this is
|
||||
# only for the case where the process is not running at all.
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -36,7 +36,16 @@ with tempfile.TemporaryDirectory() as directory:
|
||||
ensure_device_config(path)
|
||||
config = read_device_config(path)
|
||||
check("fresh config is created", path.is_file())
|
||||
check("MQTT defaults off", config.get("MQTT_ENABLED") == "false")
|
||||
# Every classroom talks to the same broker, so a fresh machine is
|
||||
# pointed at it rather than left switched off waiting for somebody to
|
||||
# remember. The rooms that were set up before there was a broker are
|
||||
# brought to the same place by classroom-migrate.
|
||||
check("the broker is on by default", config.get("MQTT_ENABLED") == "true")
|
||||
check("and points at the church broker",
|
||||
config.get("MQTT_HOST") == "10.40.0.2")
|
||||
check("on the usual port", config.get("MQTT_PORT") == "1883")
|
||||
check("with no credentials invented for it",
|
||||
config.get("MQTT_USERNAME") == "" and config.get("MQTT_PASSWORD") == "")
|
||||
check("stable remains the fresh-install default", config.get("CHANNEL") == "stable")
|
||||
|
||||
print()
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
#!/bin/bash
|
||||
set -uo pipefail
|
||||
|
||||
# What an upgrade does to a room's own settings file.
|
||||
#
|
||||
# device.conf is the one file in the classroom that belongs to the church
|
||||
# rather than to the package: the room's name, and the broker password.
|
||||
# An upgrade edits it, so an upgrade can also ruin it. These are the cases
|
||||
# that matter, run against the real code — the migration's own Python is
|
||||
# lifted out of the script rather than copied here, so a change to one
|
||||
# cannot pass by disagreeing with the other.
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
python3 - "$ROOT" <<'PY'
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
script = (root / "scripts/classroom-migrate").read_text()
|
||||
|
||||
block = re.search(r'python3 - "\$DEVICE_CONFIG" <<\'PY\'\n(.*?)\nPY\n',
|
||||
script, re.S)
|
||||
|
||||
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
|
||||
|
||||
|
||||
check("the migration's device.conf code was found", block is not None)
|
||||
|
||||
if block is None:
|
||||
sys.exit(1)
|
||||
|
||||
migration = block.group(1)
|
||||
|
||||
|
||||
def migrate(text):
|
||||
"""Run the real migration over a device.conf and return it."""
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
source = Path(directory) / "migrate.py"
|
||||
source.write_text(migration)
|
||||
config = Path(directory) / "device.conf"
|
||||
config.write_text(text)
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(source), str(config)],
|
||||
capture_output=True, text=True, check=False,
|
||||
)
|
||||
|
||||
return config.read_text(), result.stdout + result.stderr
|
||||
|
||||
|
||||
print("1. a room from the Hyprland experiment")
|
||||
|
||||
BEFORE = """# Ekklesia Classroom device configuration
|
||||
|
||||
UPDATE_URL=https://updates.riverofglorymn.org
|
||||
CHANNEL=hyprland
|
||||
LIVE_ENABLED=false
|
||||
AUDIO_OUTPUT=auto
|
||||
UI_SCALE=2.0
|
||||
ROOM=little-waders
|
||||
MQTT_ENABLED=false
|
||||
MQTT_HOST=
|
||||
MQTT_PORT=1883
|
||||
MQTT_USERNAME=classroom
|
||||
MQTT_PASSWORD=hunter2-not-a-real-one
|
||||
COMPOSITOR=hyprland
|
||||
HYPR_APPEARANCE=visual
|
||||
"""
|
||||
|
||||
after, output = migrate(BEFORE)
|
||||
|
||||
check("the broker is switched on", "MQTT_ENABLED=true" in after)
|
||||
check("and given the church address", "MQTT_HOST=10.40.0.2" in after)
|
||||
check("the port is left as it was", "MQTT_PORT=1883" in after)
|
||||
check("the room keeps its name", "ROOM=little-waders" in after)
|
||||
check("the username is untouched", "MQTT_USERNAME=classroom" in after)
|
||||
check("the password is untouched",
|
||||
"MQTT_PASSWORD=hunter2-not-a-real-one" in after)
|
||||
check("and is never printed", "hunter2" not in output)
|
||||
check("the compositor setting is gone", "COMPOSITOR" not in after)
|
||||
check("and the appearance setting with it", "HYPR_APPEARANCE" not in after)
|
||||
check("the channel moves off the experiment",
|
||||
"CHANNEL=classroom-v2" in after)
|
||||
check("the room's scale is preserved", "UI_SCALE=2.0" in after)
|
||||
check("and everything else is left alone",
|
||||
"UPDATE_URL=https://updates.riverofglorymn.org" in after
|
||||
and "AUDIO_OUTPUT=auto" in after)
|
||||
|
||||
print()
|
||||
print("2. a room already pointed at a broker of its own")
|
||||
|
||||
OWN = """CHANNEL=a-channel-somebody-named
|
||||
MQTT_ENABLED=true
|
||||
MQTT_HOST=broker.example.invalid
|
||||
MQTT_PORT=8883
|
||||
MQTT_PASSWORD=leave-me
|
||||
"""
|
||||
|
||||
after, output = migrate(OWN)
|
||||
|
||||
check("a deliberate address is not overwritten",
|
||||
"MQTT_HOST=broker.example.invalid" in after)
|
||||
check("nor a deliberate port", "MQTT_PORT=8883" in after)
|
||||
check("a channel nobody here named is left alone",
|
||||
"CHANNEL=a-channel-somebody-named" in after)
|
||||
check("and its password is still there", "MQTT_PASSWORD=leave-me" in after)
|
||||
|
||||
print()
|
||||
print("3. a room old enough to have no broker settings at all")
|
||||
|
||||
OLD = """CHANNEL=canary
|
||||
UI_SCALE=1.0
|
||||
"""
|
||||
|
||||
after, output = migrate(OLD)
|
||||
|
||||
check("the settings are added", "MQTT_ENABLED=true" in after
|
||||
and "MQTT_HOST=10.40.0.2" in after and "MQTT_PORT=1883" in after)
|
||||
check("no credentials are invented",
|
||||
"MQTT_USERNAME" not in after and "MQTT_PASSWORD" not in after)
|
||||
# stable and canary are both the sway line. A machine that has just taken
|
||||
# the Plasma package and is left pointed at one of them is handed the old
|
||||
# desktop back by the next release there: the upgrade undoing itself.
|
||||
check("canary moves onto the v2 channel", "CHANNEL=classroom-v2" in after)
|
||||
|
||||
after, _ = migrate("CHANNEL=stable\nUI_SCALE=1.0\n")
|
||||
check("and so does stable", "CHANNEL=classroom-v2" in after)
|
||||
|
||||
print()
|
||||
print("4. running it twice changes nothing the second time")
|
||||
|
||||
once, _ = migrate(BEFORE)
|
||||
twice, _ = migrate(once)
|
||||
check("the second run is a no-op", once == twice)
|
||||
|
||||
print()
|
||||
print(f"{passed} passed, {failed} failed")
|
||||
sys.exit(1 if failed else 0)
|
||||
PY
|
||||
@@ -12,6 +12,7 @@ from pathlib import Path
|
||||
root = Path(sys.argv[1])
|
||||
control = (root / "packaging/debian/control.template").read_text()
|
||||
postinst = (root / "packaging/debian/postinst").read_text()
|
||||
migrate = (root / "scripts/classroom-migrate").read_text()
|
||||
provision = (root / "scripts/classroom-provision").read_text()
|
||||
units = "\n".join(path.read_text() for path in (root / "systemd").rglob("*.service"))
|
||||
canary = (root / "update_canary.sh").read_text()
|
||||
@@ -21,7 +22,7 @@ rules = (root / "config/plasma/xdg/kwinrulesrc").read_text()
|
||||
restrictions = (root / "config/plasma/xdg/kdeglobals").read_text()
|
||||
supervision = (root / "systemd/user/plasma-plasmashell.service.d/ekklesia-classroom.conf").read_text()
|
||||
build = (root / "tools/build-release.sh").read_text()
|
||||
policy = json.loads(
|
||||
chrome_policy = json.loads(
|
||||
(root / "config/chrome/policies/managed/ekklesia-classroom.json").read_text()
|
||||
)
|
||||
|
||||
@@ -67,7 +68,8 @@ check("Avahi service and socket are masked", "systemctl mask avahi-daemon.socket
|
||||
check("printer setup is not a blocking boot dependency", "classroom-printer-setup" not in units)
|
||||
check("printer setup runs automatically in provisioning", "/usr/local/bin/classroom-printer-setup" in provision)
|
||||
check("printer provisioning is bounded", "--kill-after=5s 30s" in provision)
|
||||
check("Chrome uses the system print dialog", policy.get("DisablePrintPreview") is True)
|
||||
check("Chrome uses the system print dialog",
|
||||
chrome_policy.get("DisablePrintPreview") is True)
|
||||
|
||||
print()
|
||||
print("4. secondary windows are the window manager's business")
|
||||
@@ -103,10 +105,13 @@ check("settings install to the classroom's own directory",
|
||||
# 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",
|
||||
xdg_paths = sorted(re.findall(r'\$STAGE(/etc/xdg/[^"]+)', build))
|
||||
check("what the classroom puts under /etc/xdg is two drop-ins",
|
||||
xdg_paths == [
|
||||
"/etc/xdg/autostart/ekklesia-classroom-scale.desktop",
|
||||
"/etc/xdg/plasma-workspace/env/zz-ekklesia-classroom.sh",
|
||||
])
|
||||
check("and 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
|
||||
@@ -207,6 +212,127 @@ first_rsync = canary.index("rsync -av")
|
||||
check("Trixie validation runs before the repository copy", validation < package_copy)
|
||||
check("Trixie validation runs before any upload", validation < first_rsync)
|
||||
|
||||
print()
|
||||
print("7. one scale, applied everywhere")
|
||||
autostart = (root / "config/plasma/autostart/ekklesia-classroom-scale.desktop").read_text()
|
||||
policy = (root / "config/polkit/org.ekklesia.classroom.display.policy").read_text()
|
||||
settings_entry = (root / "config/plasma/applications/ekklesia-display-settings.desktop").read_text()
|
||||
browser = (root / "scripts/classroom-browser").read_text()
|
||||
wonderink = (root / "scripts/classroom-wonderink").read_text()
|
||||
config_module = (root / "lib/classroom_config.py").read_text()
|
||||
# UI_SCALE was packaged, clamped and documented, and nothing ever ran the
|
||||
# command that applies it. A 4K television showed a 1080p interface at
|
||||
# half size and every part of this section is a piece of that.
|
||||
check("the scale is applied at every login",
|
||||
"Exec=/usr/local/bin/classroom-scale" in autostart)
|
||||
check("by the session, which is where kscreen-doctor can be asked",
|
||||
"OnlyShowIn=KDE" in autostart)
|
||||
check("and the package installs it",
|
||||
"/etc/xdg/autostart/ekklesia-classroom-scale.desktop" in build)
|
||||
check("changing it needs an administrator password",
|
||||
"auth_admin" in policy and "classroom-set-scale" in policy)
|
||||
check("and there is no way into it from the room",
|
||||
"NoDisplay=true" in settings_entry
|
||||
and "X-KDE-Shortcuts=Meta+Ctrl+Alt+Shift+D" in settings_entry)
|
||||
check("2.0 is inside the range a room may ask for",
|
||||
"UI_SCALE_MAX = 3.0" in config_module)
|
||||
check("the browser is told the same number",
|
||||
'--force-device-scale-factor="$SCALE"' in browser)
|
||||
check("so is Wonder Ink",
|
||||
'--force-device-scale-factor="$SCALE"' in wonderink)
|
||||
check("both ask the one command for it",
|
||||
"classroom-ui-scale" in browser and "classroom-ui-scale" in wonderink)
|
||||
check("the clock is sized by the bar, not by a number",
|
||||
'autoFontAndSize", true' in layout and "fontSize" not in layout)
|
||||
|
||||
print()
|
||||
print("7b. the disc player is decided before a session starts")
|
||||
layout_unit = (root / "systemd/layout/ekklesia-classroom-layout.service").read_text()
|
||||
udev = (root / "config/udev/60-ekklesia-classroom-optical.rules").read_text()
|
||||
reset = (root / "scripts/classroom-desktop-reset").read_text()
|
||||
check("before the display manager", "Before=display-manager.service" in layout_unit)
|
||||
check("started at every boot", "WantedBy=graphical.target" in layout_unit)
|
||||
check("enabled by the package", "enable ekklesia-classroom-layout.service" in postinst)
|
||||
check("a drive plugged in later is noticed", 'ENV{ID_CDROM}=="1"' in udev)
|
||||
check("and the rules are reloaded on install", "udevadm control --reload-rules" in postinst)
|
||||
check("the login says so when the layout and the hardware disagree",
|
||||
"the disc player=" in reset)
|
||||
|
||||
print()
|
||||
print("7c. the room's own popups")
|
||||
style = (root / "config/plasma/desktoptheme/dialogs/background.svg").read_text()
|
||||
plasmarc = (root / "config/plasma/xdg/plasmarc").read_text()
|
||||
notify = (root / "config/plasma/xdg/plasmanotifyrc").read_text()
|
||||
policy_notifications = chrome_policy.get("DefaultNotificationsSetting")
|
||||
# A Plasma style is a set of pictures with a per-file fallback to Breeze.
|
||||
# Replacing the notification plasmoid's QML was the other route and a QML
|
||||
# error there is a room with no notifications at all.
|
||||
for piece in ("topleft", "top", "topright", "left", "center", "right",
|
||||
"bottomleft", "bottom", "bottomright"):
|
||||
check(f"the card has a {piece}", f'id="{piece}"' in style)
|
||||
check("the style is chosen, immutably", "name[$i]=ekklesia" in plasmarc)
|
||||
check("and the package installs it", "desktoptheme/ekklesia" in build)
|
||||
check("the colour still comes from the session's scheme",
|
||||
"ColorScheme-Background" in style)
|
||||
check("notifications stay long enough to read across a room",
|
||||
"PopupTimeout=8000" in notify)
|
||||
# The volume display is the one thing the classroom's shell package
|
||||
# overrides. Plasma 6.3 loads the on-screen display from the shell
|
||||
# package and from nowhere else — the look and feel is not consulted,
|
||||
# which was measured — so this is the supported way to replace it. The
|
||||
# fallback declaration is what keeps the package this small: without it a
|
||||
# shell package loses the desktop's own defaults, and the wallpaper with
|
||||
# them, which was also measured.
|
||||
# The volume display is Plasma's own. Replacing it needs a shell package
|
||||
# — 6.3 reads the on-screen display from there and nowhere else — and a
|
||||
# shell package with one selected builds no panel at all. Measured twice,
|
||||
# and the panel is rebuilt from the layout at every login on purpose, so
|
||||
# the room would have come up empty. The scale makes Plasma's own display
|
||||
# the right size on a television, which is what was actually wanted.
|
||||
check("no shell package of our own", "plasma/shells" not in build)
|
||||
check("and nothing selects one", "ShellPackage" not in build)
|
||||
check("the volume chime is off", 'volumeFeedback", false' in layout)
|
||||
check("and off in the setting Plasma 6 actually reads",
|
||||
"AudioFeedback[$i]=false"
|
||||
in (root / "config/plasma/xdg/plasmaparc").read_text())
|
||||
check("but the display itself is on", 'volumeOsd", true' in layout)
|
||||
check("web pages cannot put a notification on the television",
|
||||
policy_notifications == 2)
|
||||
check("and cannot ask to",
|
||||
chrome_policy.get("NotificationsAllowedForUrls") == []
|
||||
and chrome_policy.get("NotificationsBlockedForUrls") == ["*"])
|
||||
|
||||
print()
|
||||
print("7c-ii. the v2 channel is its own")
|
||||
v2 = (root / "update_classroom_v2.sh").read_text()
|
||||
# A publish script that can reach another channel's manifest is one
|
||||
# mistake away from putting the Plasma build on a sway room.
|
||||
code = "\n".join(line for line in v2.splitlines()
|
||||
if not line.lstrip().startswith("#"))
|
||||
check("it publishes to classroom-v2", "CHANNEL=classroom-v2" in code)
|
||||
check("and names no other channel anywhere it acts",
|
||||
"stable" not in code and "canary" not in code)
|
||||
check("the same Trixie gate runs first",
|
||||
code.index("validate-trixie-release.sh") < code.index("rsync"))
|
||||
check("and the manifest goes up last",
|
||||
code.rindex("manifest.json") > code.index("packages/"))
|
||||
|
||||
print()
|
||||
print("7d. the broker every room shares")
|
||||
mqtt_unit = (root / "systemd/mqtt/ekklesia-classroom-mqtt.service").read_text()
|
||||
parent_call = (root / "lib/classroom_parent_call.py").read_text()
|
||||
check("the link is restarted whatever it exits with",
|
||||
"Restart=always" in mqtt_unit)
|
||||
check("it is enabled by the package",
|
||||
"enable ekklesia-classroom-mqtt.service" in postinst)
|
||||
check("a fresh room points at the church broker",
|
||||
"MQTT_HOST=10.40.0.2" in config_module)
|
||||
check("an upgraded one is brought to the same place",
|
||||
'BROKER_HOST = "10.40.0.2"' in migrate)
|
||||
check("Parent Call tells a missing broker from an unreachable one",
|
||||
"has no church network set up" in parent_call
|
||||
and "Cannot reach the church network" in parent_call)
|
||||
|
||||
print()
|
||||
print(f"passed: {passed} failed: {failed}")
|
||||
raise SystemExit(1 if failed else 0)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ROOT="$ROOT" PYTHONPATH="$ROOT/lib" python3 - <<'PY'
|
||||
import os, pathlib, runpy, subprocess, sys, tempfile
|
||||
from classroom_config import ui_scale
|
||||
root = pathlib.Path(os.environ['ROOT'])
|
||||
helper = runpy.run_path(str(root/'scripts/classroom-set-scale'))
|
||||
original = '# keep this\nCHANNEL=classroom-v2\nMQTT_PASSWORD=literal $ and = characters\nUI_SCALE=1.25\nCUSTOM=x\nUI_SCALE=1.5\n'
|
||||
result = helper['updated'](original, 2.0)
|
||||
assert 'MQTT_PASSWORD=literal $ and = characters\n' in result
|
||||
assert result.count('UI_SCALE=') == 1
|
||||
assert helper['updated'](result, 2.0) == result
|
||||
assert 'CUSTOM=x\n' in result
|
||||
assert ui_scale({'UI_SCALE': '2'}) == 2
|
||||
assert ui_scale({'UI_SCALE': '3'}) == 3
|
||||
for bad in ('nan', 'inf', '-inf', 'oops'):
|
||||
assert ui_scale({'UI_SCALE': bad}) == 1
|
||||
assert ui_scale({'UI_SCALE': '999'}) == 3
|
||||
assert ui_scale({'UI_SCALE': '-1'}) == 0.8
|
||||
# Run the actual migration's config code in a temporary directory: no services.
|
||||
script = (root/'scripts/classroom-migrate').read_text()
|
||||
code = script.split("<<'PY'\n", 1)[1].split('\nPY\n', 1)[0]
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = pathlib.Path(tmp)/'device.conf'
|
||||
for channel in ('stable', 'canary', 'hyprland', 'classroom-v2', 'custom'):
|
||||
path.write_text(original.replace('CHANNEL=classroom-v2', 'CHANNEL='+channel))
|
||||
subprocess.run([sys.executable, '-c', code, str(path)], check=True, capture_output=True)
|
||||
expected = channel if channel in ('classroom-v2', 'custom') else 'classroom-v2'
|
||||
assert 'CHANNEL='+expected+'\n' in path.read_text()
|
||||
assert 'MQTT_PASSWORD=literal $ and = characters\n' in path.read_text()
|
||||
print('Display settings: scale bounds, config preservation, idempotence and channel migration passed.')
|
||||
PY
|
||||
@@ -158,8 +158,8 @@ check(
|
||||
'OBSOLETE_KEYS = ("COMPOSITOR", "HYPR_APPEARANCE")' in migrate,
|
||||
)
|
||||
check(
|
||||
"and comes off a channel that would undo the upgrade",
|
||||
'OBSOLETE_CHANNELS = {"hyprland"}' in migrate,
|
||||
"and comes off any channel that would undo the upgrade",
|
||||
'OBSOLETE_CHANNELS = {"stable", "canary", "hyprland"}' in migrate,
|
||||
)
|
||||
check(
|
||||
"the rest of device.conf is rewritten line by line, not regenerated",
|
||||
|
||||
@@ -207,7 +207,8 @@ 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 kwinrc kwinrulesrc kscreenlockerrc ksmserverrc \
|
||||
krunnerrc plasmanotifyrc powermanagementprofilesrc; do
|
||||
krunnerrc plasmanotifyrc powermanagementprofilesrc plasmarc \
|
||||
plasmaparc; do
|
||||
install -Dm644 \
|
||||
"$ROOT/config/plasma/xdg/${setting}" \
|
||||
"$STAGE/etc/ekklesia-classroom/xdg/${setting}"
|
||||
@@ -258,6 +259,32 @@ install -Dm644 \
|
||||
"$ROOT/config/plasma/lookandfeel/contents/layouts/org.kde.plasma.desktop-layout.js" \
|
||||
"$LOOK/contents/layouts/org.kde.plasma.desktop-layout.js"
|
||||
|
||||
# NO SHELL PACKAGE OF OUR OWN.
|
||||
#
|
||||
# Plasma 6.3 loads the volume display from the shell package and from
|
||||
# nowhere else — the look and feel is not consulted, which was measured —
|
||||
# so a shell package is the only supported way to replace it, and a
|
||||
# classroom-sized one was built and tried. With it selected, plasmashell
|
||||
# builds no panel at all: not the stock one, not ours. Measured twice, and
|
||||
# the room's panel is deleted at every login on purpose, so every login
|
||||
# would have come up with nothing on the screen.
|
||||
#
|
||||
# The volume display is Plasma's own instead. UI_SCALE makes it the right
|
||||
# size on a television, which is what the room actually needed.
|
||||
|
||||
# The room's Plasma style: the card a notification and the volume display
|
||||
# are drawn on. Only the pieces the classroom changes; everything else
|
||||
# comes from Breeze, so this can never leave a popup undrawn.
|
||||
STYLE="$STAGE/usr/share/plasma/desktoptheme/ekklesia"
|
||||
|
||||
install -Dm644 \
|
||||
"$ROOT/config/plasma/desktoptheme/metadata.json" \
|
||||
"$STYLE/metadata.json"
|
||||
|
||||
install -Dm644 \
|
||||
"$ROOT/config/plasma/desktoptheme/dialogs/background.svg" \
|
||||
"$STYLE/dialogs/background.svg"
|
||||
|
||||
for entry in "$ROOT"/config/plasma/applications/*.desktop; do
|
||||
install -Dm644 "$entry" \
|
||||
"$STAGE/usr/share/applications/$(basename "$entry")"
|
||||
@@ -339,6 +366,38 @@ install -Dm755 \
|
||||
"$ROOT/scripts/classroom-migrate" \
|
||||
"$STAGE/usr/local/bin/classroom-migrate"
|
||||
|
||||
# One number, one place to ask for it.
|
||||
install -Dm755 \
|
||||
"$ROOT/scripts/classroom-ui-scale" \
|
||||
"$STAGE/usr/local/bin/classroom-ui-scale"
|
||||
|
||||
# The room's display size: a hidden dialog behind a chord, a writer that
|
||||
# asks for an administrator password, and the one thing that applies the
|
||||
# answer at every login.
|
||||
for command in classroom-display-settings classroom-set-scale; do
|
||||
install -Dm755 \
|
||||
"$ROOT/scripts/${command}" \
|
||||
"$STAGE/usr/local/bin/${command}"
|
||||
done
|
||||
|
||||
install -Dm644 \
|
||||
"$ROOT/config/polkit/org.ekklesia.classroom.display.policy" \
|
||||
"$STAGE/usr/share/polkit-1/actions/org.ekklesia.classroom.display.policy"
|
||||
|
||||
install -Dm644 \
|
||||
"$ROOT/config/plasma/autostart/ekklesia-classroom-scale.desktop" \
|
||||
"$STAGE/etc/xdg/autostart/ekklesia-classroom-scale.desktop"
|
||||
|
||||
# The panel's disc player follows this machine's hardware, and the
|
||||
# detection has to have happened before a session starts.
|
||||
install -Dm644 \
|
||||
"$ROOT/systemd/layout/ekklesia-classroom-layout.service" \
|
||||
"$STAGE/etc/systemd/system/ekklesia-classroom-layout.service"
|
||||
|
||||
install -Dm644 \
|
||||
"$ROOT/config/udev/60-ekklesia-classroom-optical.rules" \
|
||||
"$STAGE/etc/udev/rules.d/60-ekklesia-classroom-optical.rules"
|
||||
|
||||
install -Dm644 \
|
||||
"$ROOT/lib/classroom_audio.py" \
|
||||
"$STAGE/usr/local/lib/ekklesia-classroom/classroom_audio.py"
|
||||
|
||||
@@ -13,7 +13,8 @@ SRC="${1:-/srv/classroom}"
|
||||
# XDG_CONFIG_DIRS. plasma-workspace owns files by these names in /etc/xdg
|
||||
# and two packages cannot own one path.
|
||||
for setting in kwinrc kwinrulesrc kscreenlockerrc ksmserverrc \
|
||||
krunnerrc plasmanotifyrc powermanagementprofilesrc; do
|
||||
krunnerrc plasmanotifyrc powermanagementprofilesrc plasmarc \
|
||||
plasmaparc; do
|
||||
install -Dm644 "$SRC/config/plasma/xdg/$setting" \
|
||||
"/etc/ekklesia-classroom/xdg/$setting"
|
||||
rm -f "/etc/xdg/$setting.ekklesia-old"
|
||||
@@ -48,6 +49,20 @@ install -Dm644 "$SRC/config/plasma/lookandfeel/contents/defaults" \
|
||||
install -Dm644 \
|
||||
"$SRC/config/plasma/lookandfeel/contents/layouts/org.kde.plasma.desktop-layout.js" \
|
||||
"$LNF/contents/layouts/org.kde.plasma.desktop-layout.js"
|
||||
install -Dm644 "$SRC/config/polkit/org.ekklesia.classroom.display.policy" \
|
||||
/usr/share/polkit-1/actions/org.ekklesia.classroom.display.policy
|
||||
install -Dm644 "$SRC/config/plasma/autostart/ekklesia-classroom-scale.desktop" \
|
||||
/etc/xdg/autostart/ekklesia-classroom-scale.desktop
|
||||
|
||||
install -Dm644 "$SRC/config/plasma/desktoptheme/metadata.json" \
|
||||
/usr/share/plasma/desktoptheme/ekklesia/metadata.json
|
||||
install -Dm644 "$SRC/config/plasma/desktoptheme/dialogs/background.svg" \
|
||||
/usr/share/plasma/desktoptheme/ekklesia/dialogs/background.svg
|
||||
|
||||
for script in classroom-ui-scale classroom-scale classroom-desktop-layout \
|
||||
classroom-display-settings classroom-set-scale; do
|
||||
install -Dm755 "$SRC/scripts/$script" "/usr/local/bin/$script"
|
||||
done
|
||||
|
||||
for entry in "$SRC"/config/plasma/applications/*.desktop; do
|
||||
install -Dm644 "$entry" "/usr/share/applications/$(basename "$entry")"
|
||||
|
||||
@@ -77,6 +77,38 @@ up)
|
||||
done
|
||||
# startplasma looks for `qdbus`; Debian ships it as qdbus6.
|
||||
docker exec "$NAME" ln -sf /usr/bin/qdbus6 /usr/local/bin/qdbus
|
||||
|
||||
# The church's own networks, refused from inside the container.
|
||||
#
|
||||
# This is not theoretical. The moment the package began defaulting
|
||||
# MQTT_HOST to the church broker, the lab connected to it: a test
|
||||
# machine appearing on the real broker under a container id. Nothing
|
||||
# was left behind — the broker held no retained status afterwards,
|
||||
# which was checked — but the lab had no business being there at all,
|
||||
# and the Parent Call coordinator is on the same networks.
|
||||
#
|
||||
# Container-local: this netns belongs to the container alone and
|
||||
# nothing on the host or the church network is changed by it. Debian's
|
||||
# own mirrors are untouched, so installs still work.
|
||||
# A blackhole route rather than a firewall rule: the lab image has no
|
||||
# iptables, and the rig's guard had been quietly doing nothing because
|
||||
# of it. Routes need no netfilter modules and cannot silently fail.
|
||||
docker exec "$NAME" sh -c '
|
||||
for net in 10.30.0.0/16 10.40.0.0/16; do
|
||||
ip route replace blackhole "$net" || exit 1
|
||||
done' || echo " WARNING: could not fence the church networks"
|
||||
|
||||
# And checked with bash, not sh. The probe uses /dev/tcp, which is a
|
||||
# bash feature: under dash it fails whatever the network is doing, so
|
||||
# the check passed on a lab that could reach the broker perfectly
|
||||
# well. Measured.
|
||||
for target in 10.40.0.2/1883 10.30.0.2/1025; do
|
||||
if docker exec "$NAME" timeout 4 bash -c "exec 3<>/dev/tcp/${target}" 2>/dev/null; then
|
||||
echo "REFUSING TO CONTINUE: ${target} is reachable from the lab" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo " church networks fenced out of the lab"
|
||||
echo "lab $NAME is up ($(docker exec "$NAME" systemctl is-system-running 2>/dev/null))"
|
||||
;;
|
||||
|
||||
|
||||
@@ -24,14 +24,19 @@ inlab() { docker exec "$NAME" "$@"; }
|
||||
guard() {
|
||||
# Container-local. This netns belongs to the container alone, so
|
||||
# nothing on the host or the church network is changed by it.
|
||||
# Blackhole routes, not iptables: the lab image has no iptables at
|
||||
# all, so these two lines had been failing silently since they were
|
||||
# written. `tools/lab/lab up` now does the same thing at start-up, so
|
||||
# this is a second statement of it rather than the only one.
|
||||
inlab sh -c '
|
||||
iptables -C OUTPUT -d 10.30.0.0/16 -j REJECT 2>/dev/null ||
|
||||
iptables -I OUTPUT -d 10.30.0.0/16 -j REJECT
|
||||
iptables -C OUTPUT -d 10.40.0.0/16 -j REJECT 2>/dev/null ||
|
||||
iptables -I OUTPUT -d 10.40.0.0/16 -j REJECT'
|
||||
for net in 10.30.0.0/16 10.40.0.0/16; do
|
||||
ip route replace blackhole "$net" || exit 1
|
||||
done'
|
||||
|
||||
# bash, not sh. /dev/tcp is a bash feature and this probe reported
|
||||
# every target unreachable under dash, whatever the network was doing.
|
||||
for target in 10.40.0.2/1883 10.30.0.2/1025; do
|
||||
if inlab timeout 4 sh -c "exec 3<>/dev/tcp/${target}" 2>/dev/null; then
|
||||
if inlab timeout 4 bash -c "exec 3<>/dev/tcp/${target}" 2>/dev/null; then
|
||||
echo "REFUSING TO CONTINUE: ${target} is still reachable" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Publish a build to the classroom-v2 channel.
|
||||
#
|
||||
# The v2 rooms are on a channel of their own, and classroom-migrate moves
|
||||
# a machine onto it as soon as it takes this package: stable and canary
|
||||
# are both the sway line, and a v2 room left on either is handed the old
|
||||
# desktop back by the next release there.
|
||||
#
|
||||
# Nothing here can touch stable or canary. It writes one manifest, in one
|
||||
# directory, named after the one channel this file is about.
|
||||
#
|
||||
# ./update_classroom_v2.sh 0.1.48
|
||||
#
|
||||
# The package goes up first and the manifest last, so a room that checks
|
||||
# for an update in the middle of a publish is offered nothing rather than
|
||||
# a manifest pointing at a file that is not there yet.
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
CHANNEL=classroom-v2
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 VERSION"
|
||||
echo "For example: $0 0.1.48"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version="$1"
|
||||
|
||||
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Invalid version: $version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
package_name="ekklesia-classroom_${version}_amd64.deb"
|
||||
package_path="$ROOT/dist/$package_name"
|
||||
|
||||
if [[ ! -f "$package_path" ]]; then
|
||||
echo "No such package: $package_path"
|
||||
echo "Build it first: tools/build-release.sh $version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Publishing $version to $CHANNEL"
|
||||
echo
|
||||
|
||||
# The same gate canary uses, against the exact artifact being published.
|
||||
validation_args=("$package_path")
|
||||
|
||||
if [[ -f "$ROOT/repo/$CHANNEL/manifest.json" ]]; then
|
||||
previous_name="$(python3 -c \
|
||||
"import json; print(json.load(open('repo/$CHANNEL/manifest.json'))['package'].rsplit('/', 1)[-1])")"
|
||||
previous="$ROOT/repo/packages/$previous_name"
|
||||
|
||||
if [[ -f "$previous" && "$previous" != "$package_path" ]]; then
|
||||
validation_args+=("$previous")
|
||||
fi
|
||||
fi
|
||||
|
||||
"$ROOT/tools/validate-trixie-release.sh" "${validation_args[@]}"
|
||||
|
||||
hash="$(sha256sum "$package_path" | awk '{print $1}')"
|
||||
|
||||
mkdir -p "$ROOT/repo/packages" "$ROOT/repo/$CHANNEL"
|
||||
cp "$package_path" "$ROOT/repo/packages/$package_name"
|
||||
|
||||
cat > "$ROOT/repo/$CHANNEL/manifest.json" <<MANIFEST
|
||||
{
|
||||
"version": "$version",
|
||||
"package": "/packages/$package_name",
|
||||
"sha256": "$hash",
|
||||
"requires_reboot": true
|
||||
}
|
||||
MANIFEST
|
||||
|
||||
echo
|
||||
echo "Publishing package..."
|
||||
|
||||
rsync -av -e ssh \
|
||||
"$ROOT/repo/packages/$package_name" \
|
||||
classroom-control:/srv/ekklesia/classroom-updates/packages/
|
||||
|
||||
echo
|
||||
echo "Publishing $CHANNEL manifest..."
|
||||
|
||||
# Manifest deliberately goes LAST.
|
||||
rsync -av -e ssh \
|
||||
"$ROOT/repo/$CHANNEL/manifest.json" \
|
||||
"classroom-control:/srv/ekklesia/classroom-updates/$CHANNEL/manifest.json"
|
||||
|
||||
echo
|
||||
echo "Published Ekklesia Classroom $version to $CHANNEL"
|
||||
echo "https://updates.riverofglorymn.org/$CHANNEL/manifest.json"
|
||||