Files
Classroom_Desktop/scripts/classroom-notify-test
T
ekklesiaandClaude Opus 5 ffd18edf7e fix: deliver test notifications into the classroom session
The classroom desktop belongs to the classroom user and mako sits on
that user's D-Bus session. An administrator over SSH is in a different
session where nothing owns org.freedesktop.Notifications, so notify-send
asked D-Bus to start a second mako, which had no display to open and
timed out with NoReply.

Find the running sway process for the classroom user and deliver into
its session instead. XDG_RUNTIME_DIR and the bus address are read from
that process, falling back to /run/user/<uid> and its bus socket when
the environment cannot be read — root has ptrace access on the endpoint,
but that is not somewhere to be brittle. The Wayland display is read the
same way, and failing that from the wayland-N.lock sway holds open, so
no socket name is ever assumed. notify-send only needs the bus, so a
display that cannot be resolved is not worth refusing to send over.

Refuses to send when mako is not already running, rather than letting
D-Bus start one, and says plainly what is wrong when there is no
session, no privileges, or no bus socket. --dry-run reports what it
resolved without sending.

Behaviour inside the session is unchanged, and the sample notifications
and styling are untouched.

The tests stand a real background process in for sway so /proc/<pid>/environ
is genuine rather than mocked, and cover both invocation paths plus the
fallbacks and every refusal. The build runs them.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-04 17:38:44 -05:00

205 lines
6.2 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
# Sends a sample notification so the classroom styling can be checked
# without waiting for a real event.
#
# classroom-notify-test
# classroom-notify-test "Title" "Body text"
# classroom-notify-test --critical "Parent Call" "Room 3 needs a parent."
# sudo classroom-notify-test --dry-run
#
# The classroom desktop runs as the classroom user, and mako is attached
# to that user's D-Bus 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 second mako, which 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
;;
--dry-run)
dry_run=true
shift
;;
--)
shift
break
;;
-*)
echo "classroom-notify-test: 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-test: $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
}
# sway 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.
sway_pid="n/a"
runtime_dir="${XDG_RUNTIME_DIR:-}"
bus="$DBUS_SESSION_BUS_ADDRESS"
wayland_display="$WAYLAND_DISPLAY"
else
session="the $CLASSROOM_USER desktop session"
sway_pid="$(pgrep -u "$CLASSROOM_USER" -x sway | head -n 1 || true)"
[[ -n "$sway_pid" ]] ||
fail "no classroom desktop session is running: there is no sway 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-test) or as the $CLASSROOM_USER user."
fi
environment_block="$(read_environ "$sway_pid")"
if [[ -n "$environment_block" ]]; then
source_note="read from sway (pid $sway_pid)"
else
source_note="sway'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 "$sway_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 mako that is already running. Sending into a session
# without one would have D-Bus start a second mako, which is exactly the
# failure this script exists to avoid.
pgrep -u "$CLASSROOM_USER" -x mako >/dev/null 2>&1 ||
fail "mako is not running in the classroom session, and this will not start it. Check with: pgrep -u $CLASSROOM_USER -a mako"
if [[ "$dry_run" == true ]]; then
echo "Would notify $session:"
echo " sway pid: $sway_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"