diff --git a/tools/lab/Dockerfile.plasma b/tools/lab/Dockerfile.plasma new file mode 100644 index 0000000..58c6acd --- /dev/null +++ b/tools/lab/Dockerfile.plasma @@ -0,0 +1,52 @@ +# Classroom v2 test lab: a full Debian 13 Plasma 6 desktop, running under +# systemd, inside a container. It exists because the development host is +# itself a VM with no /dev/kvm, so a real virtual machine cannot be created +# here. See docs/classroom-v2-experiment.md. +# +# docker build -f tools/lab/Dockerfile.plasma -t classroom-v2-lab:plasma . +# tools/lab/lab up +# +FROM debian:trixie + +ENV DEBIAN_FRONTEND=noninteractive +ENV container=docker + +# systemd, so the session is supervised the way it would be on a real machine +RUN apt-get update && apt-get install -y systemd systemd-sysv dbus dbus-user-session \ + && rm -rf /var/lib/apt/lists/* + +# The desktop under test. +RUN apt-get update && apt-get install -y \ + plasma-desktop plasma-workspace kwin-wayland \ + plasma-nm plasma-pa \ + && rm -rf /var/lib/apt/lists/* + +# Software rendering, X11 apps, fonts, and the tools the lab drives it with. +RUN apt-get update && apt-get install -y --no-install-recommends \ + libgl1-mesa-dri libglx-mesa0 mesa-utils-bin libegl-mesa0 \ + xwayland \ + fonts-noto-core fonts-dejavu-core fonts-liberation \ + kde-cli-tools qdbus-qt6 \ + sway swaybg grim wtype foot \ + python3 python3-gi gir1.2-gtk-3.0 \ + curl ca-certificates gnupg procps psmisc less vim-tiny socat jq \ + && rm -rf /var/lib/apt/lists/* + +RUN useradd -m -u 1000 -s /bin/bash classroom \ + && usermod -aG video,render,audio classroom \ + && passwd -d classroom \ + && mkdir -p /run/user/1000 && chown classroom:classroom /run/user/1000 \ + && systemctl enable systemd-logind + +# Linger so the classroom user's systemd instance starts with the container, +# which is exactly how the appliance would run. +RUN mkdir -p /var/lib/systemd/linger && touch /var/lib/systemd/linger/classroom + +STOPSIGNAL SIGRTMIN+3 +CMD ["/sbin/init"] + +# Screenshots. KWin's org.kde.KWin.ScreenShot2 only answers callers it +# recognises by path, and Spectacle is one of them, so this is the +# sanctioned way to capture a KWin session that has no outer compositor. +RUN apt-get update && apt-get install -y --no-install-recommends kde-spectacle \ + && rm -rf /var/lib/apt/lists/* diff --git a/tools/lab/lab b/tools/lab/lab new file mode 100755 index 0000000..714865c --- /dev/null +++ b/tools/lab/lab @@ -0,0 +1,176 @@ +#!/bin/bash +# Classroom v2 test lab driver. +# +# The development host is a VM with no /dev/kvm, so the desktop under test +# runs inside a systemd container instead of a virtual machine. Nothing this +# script does touches the host outside Docker. +# +# The session is nested: an outer headless sway exists only to give the +# desktop under test a display that grim can capture and that sway's IPC can +# aim a real pointer at. The outer compositor is lab equipment, not part of +# the classroom design. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +NAME="${LAB_NAME:-classroom-v2-lab}" +IMAGE="${LAB_IMAGE:-classroom-v2-lab:plasma}" +DOCKERFILE="${LAB_DOCKERFILE:-tools/lab/Dockerfile.plasma}" +SHOTS="${LAB_SHOTS:-$ROOT/docs/screenshots}" + +OUTER=wayland-1 + +session_env() { + docker exec -u classroom \ + -e XDG_RUNTIME_DIR=/run/user/1000 \ + -e HOME=/home/classroom \ + -e LANG=C.UTF-8 \ + -e DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \ + "$@" +} + +# Talk to the outer lab compositor: grim, swaymsg. +outer() { + local sock + sock=$(docker exec "$NAME" sh -c 'ls /run/user/1000/sway-ipc.* 2>/dev/null | head -1') + session_env -e WAYLAND_DISPLAY="$OUTER" -e SWAYSOCK="$sock" "$NAME" "$@" +} + +# Talk to the Plasma session under test. +inner() { + session_env -e WAYLAND_DISPLAY=wayland-0 -e XDG_CURRENT_DESKTOP=KDE \ + -e QT_QPA_PLATFORM=wayland "$NAME" "$@" +} + +case "${1:-}" in +build) + shift + exec docker build -f "$ROOT/$DOCKERFILE" -t "$IMAGE" "$@" "$ROOT" + ;; + +up) + docker rm -f "$NAME" >/dev/null 2>&1 || true + docker run -d --name "$NAME" \ + --privileged --cgroupns=private \ + --tmpfs /run --tmpfs /run/lock \ + --shm-size=1g \ + -v "$ROOT:/srv/classroom:ro" \ + "$IMAGE" >/dev/null + for _ in $(seq 1 90); do + state=$(docker exec "$NAME" systemctl is-system-running 2>/dev/null || true) + [[ "$state" == running || "$state" == degraded ]] && break + sleep 1 + done + for _ in $(seq 1 30); do + docker exec "$NAME" test -S /run/user/1000/bus && break + sleep 1 + done + # startplasma looks for `qdbus`; Debian ships it as qdbus6. + docker exec "$NAME" ln -sf /usr/bin/qdbus6 /usr/local/bin/qdbus + echo "lab $NAME is up ($(docker exec "$NAME" systemctl is-system-running 2>/dev/null))" + ;; + +down) + docker rm -f "$NAME" >/dev/null 2>&1 || true + echo "lab $NAME is down" + ;; + +outer-up) + session_env \ + -e WLR_BACKENDS=headless -e WLR_LIBINPUT_NO_DEVICES=1 \ + -e WLR_RENDERER=pixman -e LIBGL_ALWAYS_SOFTWARE=1 \ + -d "$NAME" sh -c 'exec sway -c /srv/classroom/tools/lab/outer-sway.conf \ + > /tmp/outer-sway.log 2>&1' + for _ in $(seq 1 30); do + docker exec "$NAME" test -S "/run/user/1000/$OUTER" && break + sleep 1 + done + echo "outer compositor on $OUTER" + ;; + +plasma-up) + session_env \ + -e WAYLAND_DISPLAY="$OUTER" -e XDG_SESSION_TYPE=wayland \ + -e XDG_CURRENT_DESKTOP=KDE -e LIBGL_ALWAYS_SOFTWARE=1 \ + -d "$NAME" sh -c 'exec startplasma-wayland > /tmp/plasma.log 2>&1' + for _ in $(seq 1 60); do + docker exec "$NAME" pgrep -x plasmashell >/dev/null 2>&1 && break + sleep 1 + done + sleep 5 + echo "plasma session up" + ;; + +plasma-down) + docker exec "$NAME" pkill -u classroom -f startplasma-wayland || true + docker exec "$NAME" pkill -u classroom -x plasmashell || true + docker exec "$NAME" pkill -u classroom -x kwin_wayland || true + sleep 2 + echo "plasma session down" + ;; + +session) + "$0" outer-up + "$0" plasma-up + ;; + +shot) + # shot NAME [--delay SECONDS] + name="${2:?usage: lab shot NAME}" + outer grim /tmp/lab-shot.png >/dev/null + mkdir -p "$SHOTS" + docker exec "$NAME" cat /tmp/lab-shot.png > "$SHOTS/$name.png" + echo "$SHOTS/$name.png" + ;; + +click) + # click X Y — a real pointer press in the session under test + outer swaymsg seat - cursor set "$2" "$3" >/dev/null + sleep 0.3 + outer swaymsg seat - cursor press button1 >/dev/null + sleep 0.1 + outer swaymsg seat - cursor release button1 >/dev/null + ;; + +move) + outer swaymsg seat - cursor set "$2" "$3" >/dev/null + ;; + +key) + shift + outer wtype "$@" + ;; + +sh) + shift + exec docker exec -it "$NAME" bash "$@" + ;; + +run) shift; session_env "$NAME" "$@" ;; +irun) shift; session_env -it "$NAME" "$@" ;; +inner) shift; inner "$@" ;; +inner-it) shift; session_env -it -e WAYLAND_DISPLAY=wayland-0 -e XDG_CURRENT_DESKTOP=KDE "$NAME" "$@" ;; +outer) shift; outer "$@" ;; + +*) + cat <