#!/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"