test: a lab that runs a real desktop where no VM can exist

The development host is a VM without /dev/kvm, so a candidate desktop
cannot be given a virtual machine here. It gets a systemd container
instead, with the session nested inside a headless sway so that grim can
photograph it and sway's IPC can aim a real pointer at it.

Plasma 6.3.6 comes up in it, software-rendered, at 1920x1080.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
ekklesia
2026-09-10 07:59:46 -05:00
co-authored by Claude Opus 5
parent 71db7fd6ec
commit bfc7caf28f
3 changed files with 242 additions and 0 deletions
+52
View File
@@ -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/*
Executable
+176
View File
@@ -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 <<USAGE
usage: tools/lab/lab COMMAND
build build the lab image
up start the lab container (systemd inside)
down remove it
session outer compositor + a Plasma session on top of it
outer-up just the outer compositor
plasma-up just the Plasma session
plasma-down stop the Plasma session
shot NAME capture the screen to docs/screenshots/NAME.png
click X Y press the pointer at a screen position
move X Y move the pointer
key ARGS... wtype, aimed at the session
inner CMD... run CMD as a client of the Plasma session
outer CMD... run CMD against the lab compositor
run CMD... run CMD as the classroom user, no display
sh root shell in the lab
USAGE
exit 1
;;
esac
+14
View File
@@ -0,0 +1,14 @@
# Outer compositor for the classroom v2 lab.
#
# It exists only to give the desktop under test a Wayland display it can
# nest inside, so that grim can capture it and sway's IPC can drive a real
# pointer at it. It is not part of the classroom design.
output HEADLESS-1 resolution 1920x1080 position 0 0
output HEADLESS-1 bg #101010 solid_color
default_border none
default_floating_border none
focus_follows_mouse no
xwayland disable
# Nothing may steal a key or a click from the session under test.