Building the package showed what reading the tree did not: classroom-dvd still calls classroom-window, classroom-mqtt still calls classroom-notify, and the DVD overlay still imports the icon and stylesheet helpers the dock shared. They go back in, and the write-up now counts what was actually removed rather than what could be. 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="Ekklesia Classroom" \
|
|
--urgency="$urgency" \
|
|
-- \
|
|
"$title" "$body"
|
|
|
|
echo "Sent $urgency notification to $session: $title"
|