Parent Call told rooms they had never been set up. Nothing about the rooms was wrong. Read from the church broker: …/coordinator and …/pending are retained and …/mode is absent, and the coordinator's own source says why — it publishes the retained mode only in answer to an operator writing to …/mode/set, and nobody ever has. Every classroom waited a second and a half for a message that will never exist. A missing mode is now read as the coordinator's own default rather than as an unconfigured room: the room offers the call and lets the sanctuary end answer, because that is the end that knows. A mode that really is off still refuses, in the coordinator's words, and a broker that is missing or unreachable still says which of those it is. Proved against a rig broker made to match the church's exactly, with a call landing on the sanctuary mock. The card is anchored to the bottom-left corner instead of centred, by layer shell, which is what the disc player's cover has used under KWin for months. Anchoring rather than positioning is what makes it right at 1080p and at 4K without a coordinate anywhere. It falls back to the centred card if the library is missing. Notifications are sized for a room. Every Plasma measurement is a grid unit and a grid unit is the height of the general font, so the font is the lever — 15 point instead of Breeze's 10, which takes the card from about 350x110 to about 410x145 at 100% and doubles again at 200%. Text, icon, padding and buttons move together. The chime stays off. No shell package: that path cost the panel and is not coming back. And the product name is gone from everything a teacher sees. The notification card is from Classroom, the disc player's from DVD. Co-Authored-By: Claude Opus 5 <[email protected]>
217 lines
6.7 KiB
Bash
Executable File
217 lines
6.7 KiB
Bash
Executable File
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
# Puts a notification on the classroom screen, from anywhere.
|
|
#
|
|
# classroom-notify "Title" "Body text"
|
|
# classroom-notify --critical "Parent Call" "Room 3 needs a parent."
|
|
# classroom-notify --urgency=low "DVD" "Disc ejected."
|
|
# sudo classroom-notify --dry-run
|
|
#
|
|
# Used by classroom-notify-test to send a sample, and by classroom-mqtt
|
|
# to put messages from the church broker on the screen.
|
|
#
|
|
# The classroom desktop runs as the classroom user, and the notification
|
|
# service is part of that user's Plasma session. An administrator over SSH
|
|
# sits in a different session where nothing owns
|
|
# org.freedesktop.Notifications, so a plain notify-send there asks D-Bus to
|
|
# start a service that has no display to open, and times out with NoReply.
|
|
# Find the running desktop session and deliver into it instead.
|
|
|
|
CLASSROOM_USER=classroom
|
|
|
|
urgency=normal
|
|
dry_run=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--critical)
|
|
urgency=critical
|
|
shift
|
|
;;
|
|
--low)
|
|
urgency=low
|
|
shift
|
|
;;
|
|
--urgency=*)
|
|
urgency="${1#--urgency=}"
|
|
case "$urgency" in
|
|
low|normal|critical) ;;
|
|
*) urgency=normal ;;
|
|
esac
|
|
shift
|
|
;;
|
|
--dry-run)
|
|
dry_run=true
|
|
shift
|
|
;;
|
|
--)
|
|
shift
|
|
break
|
|
;;
|
|
-*)
|
|
echo "classroom-notify: unknown option $1" >&2
|
|
echo "usage: classroom-notify-test [--low|--critical] [--dry-run] [title] [body]" >&2
|
|
exit 2
|
|
;;
|
|
*)
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
|
|
title="${1:-DVD Ready}"
|
|
body="${2:-A disc is ready to play.}"
|
|
|
|
fail() {
|
|
echo "classroom-notify: $1" >&2
|
|
exit 1
|
|
}
|
|
|
|
# Another process's environment, or nothing. Reading this needs ptrace
|
|
# access, which root has on the endpoint but not, for instance, inside a
|
|
# container with CAP_SYS_PTRACE dropped — so every caller has to cope
|
|
# with getting nothing back.
|
|
read_environ() {
|
|
{ tr '\0' '\n' < "/proc/$1/environ"; } 2>/dev/null || true
|
|
}
|
|
|
|
environ_value() {
|
|
printf '%s\n' "$1" | sed -n "s/^$2=//p" | head -n 1
|
|
}
|
|
|
|
# The compositor holds its own wayland-N.lock open, so the display name can be read
|
|
# back off the process instead of being guessed at.
|
|
display_from_lock() {
|
|
local link target
|
|
|
|
for link in "/proc/$1/fd"/*; do
|
|
target="$(readlink "$link" 2>/dev/null || true)"
|
|
|
|
case "$target" in
|
|
*/wayland-*.lock)
|
|
target="${target%.lock}"
|
|
echo "${target##*/}"
|
|
return 0
|
|
;;
|
|
esac
|
|
done
|
|
|
|
return 0
|
|
}
|
|
|
|
classroom_uid="$(id -u "$CLASSROOM_USER" 2>/dev/null || true)"
|
|
|
|
[[ -n "$classroom_uid" ]] ||
|
|
fail "there is no $CLASSROOM_USER user on this machine."
|
|
|
|
current_uid="$(id -u)"
|
|
|
|
runner=()
|
|
session="this session"
|
|
source_note="inherited from this session"
|
|
|
|
if [[ "$current_uid" == "$classroom_uid" &&
|
|
-n "${WAYLAND_DISPLAY:-}" &&
|
|
-n "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then
|
|
# Already inside the classroom desktop session.
|
|
session_pid="n/a"
|
|
runtime_dir="${XDG_RUNTIME_DIR:-}"
|
|
bus="$DBUS_SESSION_BUS_ADDRESS"
|
|
wayland_display="$WAYLAND_DISPLAY"
|
|
else
|
|
session="the $CLASSROOM_USER desktop session"
|
|
|
|
# The compositor is the session: it owns the Wayland display and the
|
|
# environment every other part of the session inherited.
|
|
session_pid="$(pgrep -u "$CLASSROOM_USER" -x kwin_wayland | head -n 1 || true)"
|
|
|
|
[[ -n "$session_pid" ]] ||
|
|
fail "no classroom desktop session is running: there is no kwin_wayland process for user $CLASSROOM_USER."
|
|
|
|
# Checked before anything that needs to look inside the session, so
|
|
# an administrator gets told what to do rather than a permission
|
|
# error from somewhere further down.
|
|
if [[ "$current_uid" != "$classroom_uid" && "$current_uid" != "0" ]]; then
|
|
fail "run this as root (sudo classroom-notify) or as the $CLASSROOM_USER user."
|
|
fi
|
|
|
|
environment_block="$(read_environ "$session_pid")"
|
|
|
|
if [[ -n "$environment_block" ]]; then
|
|
source_note="read from the compositor (pid $session_pid)"
|
|
else
|
|
source_note="the compositor's environment was unreadable; using session defaults"
|
|
fi
|
|
|
|
runtime_dir="$(environ_value "$environment_block" XDG_RUNTIME_DIR)"
|
|
runtime_dir="${runtime_dir:-/run/user/$classroom_uid}"
|
|
|
|
[[ -d "$runtime_dir" ]] ||
|
|
fail "the classroom session runtime directory $runtime_dir does not exist."
|
|
|
|
bus="$(environ_value "$environment_block" DBUS_SESSION_BUS_ADDRESS)"
|
|
bus="${bus:-unix:path=$runtime_dir/bus}"
|
|
|
|
case "$bus" in
|
|
unix:path=*)
|
|
bus_socket="${bus#unix:path=}"
|
|
bus_socket="${bus_socket%%,*}"
|
|
|
|
[[ -S "$bus_socket" ]] ||
|
|
fail "the classroom session bus socket $bus_socket does not exist. Is the desktop session fully up?"
|
|
;;
|
|
esac
|
|
|
|
wayland_display="$(environ_value "$environment_block" WAYLAND_DISPLAY)"
|
|
|
|
if [[ -z "$wayland_display" ]]; then
|
|
wayland_display="$(display_from_lock "$session_pid")"
|
|
fi
|
|
|
|
environment=(
|
|
"XDG_RUNTIME_DIR=$runtime_dir"
|
|
"DBUS_SESSION_BUS_ADDRESS=$bus"
|
|
)
|
|
|
|
# notify-send only needs the bus. The display is passed through for
|
|
# the sake of anything else in the session, and its absence is not
|
|
# worth refusing to send over.
|
|
if [[ -n "$wayland_display" ]]; then
|
|
environment+=("WAYLAND_DISPLAY=$wayland_display")
|
|
fi
|
|
|
|
if [[ "$current_uid" == "$classroom_uid" ]]; then
|
|
runner=(env "${environment[@]}")
|
|
else
|
|
runner=(runuser -u "$CLASSROOM_USER" -- env "${environment[@]}")
|
|
fi
|
|
fi
|
|
|
|
# Deliver to the notification service that is already running. Sending into
|
|
# a session without one would have D-Bus start something with no display to
|
|
# draw on, which is exactly the failure this script exists to avoid.
|
|
pgrep -u "$CLASSROOM_USER" -x plasmashell >/dev/null 2>&1 ||
|
|
fail "the classroom shell is not running, and this will not start it. Check with: pgrep -u $CLASSROOM_USER -a plasmashell"
|
|
|
|
if [[ "$dry_run" == true ]]; then
|
|
echo "Would notify $session:"
|
|
echo " compositor pid: $session_pid"
|
|
echo " session environment: $source_note"
|
|
echo " XDG_RUNTIME_DIR: $runtime_dir"
|
|
echo " DBUS_SESSION_BUS_ADDRESS: $bus"
|
|
echo " WAYLAND_DISPLAY: ${wayland_display:-(not resolved)}"
|
|
echo " urgency: $urgency"
|
|
echo " title: $title"
|
|
echo " body: $body"
|
|
exit 0
|
|
fi
|
|
|
|
"${runner[@]}" notify-send \
|
|
--app-name="Classroom" \
|
|
--urgency="$urgency" \
|
|
-- \
|
|
"$title" "$body"
|
|
|
|
echo "Sent $urgency notification to $session: $title"
|