vmbox: vm lightdm and vm shim — the host's login topology and fake hardware
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run

The golden image logs in with agetty+startx and has no RGB controllers,
no NVIDIA card and no sound card, so the night-lockdown scripts — which
mask/stop lightdm.service and getty@.service and shell out to openrgb,
nvidia-smi and amixer — had nothing real to act on in a sandbox and
were being "verified" on the host instead.

- `vm lightdm <name>`: installs lightdm + lightdm-gtk-greeter into the
  live overlay (pacman -Syu, not -S: the overlay's package database is
  the image's and mirrors have rotated those versions out), configures
  autologin -> i3 exactly like the host, turns tty1 back into a plain
  getty, reboots the guest and proves lightdm.service is active with an
  Xorg it owns. Never rebuilds the base: `vm build --force` re-seals a
  new checksum while every existing sandbox still backs onto the old
  file's name, which is silent qcow2 corruption.
- `vm shim <name> <tool> [exit[:N]|hang|sleep:S]`: a fake tool ahead of
  the real one on PATH, every call logged with an epoch to
  /var/log/vmbox-shim/<tool>.log so the host can assert what ran and in
  what order. `hang` is the openrgb 1.0-2 that never returned on
  2026-09-12 and defeated the curfew.
- The DISPLAY/XAUTHORITY resolver `vm run` sources becomes its own file,
  guest/vmbox-x11.sh, installed by the image build and re-installed by
  the lightdm switch; it now tests readability, not existence, and tries
  ~/.Xauthority before the startx serverauth glob, because lightdm's
  Xorg -auth file is root-only.
- README: the worked example uses MIDNIGHT_SHUTDOWN_CONFIRM=y (the
  installer refuses to run unattended now; `echo y |` no longer works),
  and the "i2c RGB stays on the host" line is gone.
- bats: four host-side tests for shim.sh (exit, exit:N, hang, refusals).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mLnUeoJ3mUwGRTowTUH6d
This commit is contained in:
Krzysztof Rudnicki 2026-09-13 10:51:10 +02:00
parent 96f3bb5326
commit 00bb6706fd
9 changed files with 354 additions and 65 deletions

View File

@ -71,15 +71,22 @@ the discriminator, and it is asserted by grep, not read by eye.
## Worked example: testing the real shutdown installer
```bash
vm share ~/src/testsAndMisc # read-only bind, not a symlink
vm share ~/src/utils # guard-lib lives here
vm share ~/src/digital-wellbeing # read-only bind, not a symlink
vm share ~/src/utils/guard-lib # the config guard it installs
vm new st --rtc 2026-08-22T20:55:00 # inside the 21:00-05:00 window
vm run st 'git clone --no-hardlinks -q /mnt/hostrepo/testsAndMisc ~/tam'
vm run st 'echo y | sudo bash ~/tam/linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh enable'
vm repo st ~/src/digital-wellbeing --worktree
vm repo st ~/src/utils/guard-lib --worktree
vm run st 'sudo bash ~/guard-lib/install.sh'
vm run st 'sudo MIDNIGHT_SHUTDOWN_CONFIRM=y bash ~/digital-wellbeing/setup_midnight_shutdown.sh enable'
vm run st 'sudo systemctl poweroff' # -> VERDICT: clean poweroff
vm reset st # back to pristine
```
The full night-lockdown flow (lightdm topology, a hanging `openrgb` shim, the
per-minute timer, mask/stop, black screen, morning unlock) is scripted in
`digital-wellbeing/tests/vmbox_night_lockdown.sh` and is the reference for
how `vm lightdm` and `vm shim` fit together.
Verified: the config lands at `/etc/shutdown-schedule.conf` under guard-lib's
immutable protection, and `shutdown-timer-monitor.service`,
`day-specific-shutdown.timer` and `shutdown-timer-monitor-watchdog.timer` all
@ -88,9 +95,10 @@ timestamp and `/etc/hosts` kept its `+i` flag.
Two things that installer taught us, both worth knowing before you run it:
- **It is interactive.** It ends on `read -r -p 'Do you want to proceed?'`.
- **It refuses to run unattended.** It ends on a confirmation prompt, and
`vm run` closes stdin on purpose (installers that prompt would otherwise
hang forever), so pipe `echo y |` when you actually want it to proceed.
hang forever). Piping `echo y |` no longer works either -- with no terminal
it declines by design. Set `MIDNIGHT_SHUTDOWN_CONFIRM=y` to proceed.
- **It is subcommand-driven** (`enable` / `status`). Bare invocation prints
usage and exits 1 without changing anything.
@ -119,14 +127,33 @@ Two things that installer taught us, both worth knowing before you run it:
| `vm run <name> <cmd...>` | Run a command, print the verdict |
| `vm ssh <name>` | Interactive shell (starts the VM if needed) |
| `vm screenshot <name> [out.png]` | Capture the screen (locker/X11 tests) — verified: real 1280x800 PNG of the guest's i3 session, no viewer installed |
| `vm lightdm <name>` | Switch the sandbox to the host's login topology: lightdm autologin → i3, tty1 a plain getty. Reboots the guest and proves `lightdm.service` is active with an Xorg it owns |
| `vm shim <name> <tool> [exit[:N]\|hang\|sleep:S]` | Put a fake `<tool>` ahead of the real one on PATH (`/usr/local/sbin`). Every call is logged with an epoch to `/var/log/vmbox-shim/<tool>.log`, so the host can assert what ran and in what order. `hang` reproduces a tool that never returns |
| `vm reset <name>` | Wipe back to pristine |
| `vm rm <name>` / `vm list` | Delete / list sandboxes |
## Hardware the guest does not have: shim it
The guest has no RGB controllers, no NVIDIA card and no sound card, and the
golden image logs in with agetty+startx rather than a display manager. None
of that is a reason to test on the host. `vm lightdm` gives a sandbox the
host's login units (the night-lockdown scripts mask/stop `lightdm.service`
and `getty@.service`, which do not exist under startx), and `vm shim` stands
in for `openrgb`, `nvidia-smi`, `amixer` with a chosen behaviour and a call
log. The 2026-09-12 curfew failure -- openrgb 1.0-2 never returning, before
the teardown -- is `vm shim nl openrgb hang`, and the fix is proven by the
lockdown completing anyway.
Both mutate a live overlay only. The base image is never rebuilt for this:
`vm build --force` re-seals a new checksum while every existing sandbox still
backs onto the old file's *name*, which is silent qcow2 corruption, not a
loud refusal.
## Out of scope
A VM cannot cover everything: adb/phone tests (~36 files), GPU monitors, the
i2c RGB in `setup_night_lockdown.sh`, and the Nextcloud/SearXNG suites (whose
fixtures assume a Debian layout) all stay on the host or stay untested.
A VM still cannot cover adb/phone tests (~36 files), real GPU monitors, and
the Nextcloud/SearXNG suites (whose fixtures assume a Debian layout); those
stay on the host or stay untested.
## Tests

View File

@ -28,6 +28,10 @@ Usage: vm <command> [args]
repo <name> <path> [--worktree]
Copy a host repo in (clone=HEAD, --worktree=uncommitted)
screenshot <name> [out.png] Capture the guest screen (for locker tests)
lightdm <name> Switch the sandbox to lightdm autologin -> i3,
tty1 a plain getty (the host's login topology)
shim <name> <tool> [mode] Fake a hardware tool on PATH: exit[:N] | hang |
sleep:S; calls logged in /var/log/vmbox-shim/
reset <name> Wipe back to pristine (sub-second)
rm <name> Delete the sandbox entirely
list List sandboxes and their state
@ -95,6 +99,8 @@ main() {
ssh) source "$VMBOX_LIB_DIR/ssh.sh"; vm_ssh_interactive "$@" ;;
scp) source "$VMBOX_LIB_DIR/ssh.sh"; cmd_scp "$@" ;;
screenshot) source "$VMBOX_LIB_DIR/screenshot.sh"; vm_screenshot "$@" ;;
lightdm) require_host_deps; source "$VMBOX_LIB_DIR/desktop.sh"; cmd_lightdm "$@" ;;
shim) require_host_deps; source "$VMBOX_LIB_DIR/desktop.sh"; cmd_shim "$@" ;;
repo) source "$VMBOX_LIB_DIR/repo.sh"; cmd_repo "$@" ;;
share) source "$VMBOX_LIB_DIR/repo.sh"; repo_share "$@" ;;
unshare) source "$VMBOX_LIB_DIR/repo.sh"; repo_unshare "$@" ;;

View File

@ -0,0 +1,69 @@
#!/bin/bash
# ============================================================================
# Runs INSIDE a sandbox as root, via `vm lightdm <name>`.
#
# Switch the guest's graphical session from the golden image's agetty+startx
# autologin to a display manager, mirroring kuhy's real machine: lightdm with
# autologin straight into i3, tty1 a plain login. Scripts that mask/stop
# lightdm.service and getty@.service (night-lockdown) have nothing to act on
# under startx; here they get the same units the host has.
#
# Applied to a live overlay, never to the base image: `vm build --force`
# would silently corrupt every existing sandbox (their backing file changes
# underneath them), so the topology change is a per-sandbox step instead.
# Takes effect after a reboot, which the host side performs.
# ============================================================================
set -euo pipefail
GUEST_USER="${1:-arch}"
readonly MARKER="/etc/vmbox-desktop"
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "$MARKER" && "$(cat "$MARKER")" == "lightdm" ]]; then
echo "desktop-lightdm: already converted"
exit 0
fi
# -Syu, not -S: the overlay's package database is the golden image's, so a
# plain -S asks mirrors for versions they have already rotated out (404 on
# adwaita-fonts, seen 2026-09-13), and a partial upgrade is unsupported anyway.
# The host's pacman cache is mounted read-only, so most of this is local.
pacman -Syu --noconfirm --needed lightdm lightdm-gtk-greeter >/dev/null
echo "desktop-lightdm: lightdm installed"
# Same shape as the host's /etc/lightdm/lightdm.conf: autologin with no
# greeter delay, into the i3 session that i3-wm ships in /usr/share/xsessions.
# Arch's lightdm only honours autologin for members of the `autologin` group.
groupadd -r -f autologin
gpasswd -a "$GUEST_USER" autologin >/dev/null
install -d -m 755 /etc/lightdm/lightdm.conf.d
cat > /etc/lightdm/lightdm.conf.d/50-vmbox-autologin.conf <<CONF
[Seat:*]
greeter-session=lightdm-gtk-greeter
user-session=i3
autologin-user=${GUEST_USER}
autologin-user-timeout=0
autologin-session=i3
CONF
# tty1 becomes a plain getty, as on the host: drop the autologin drop-in and
# the startx-on-tty1 hook, or two X servers would fight over the seat.
rm -f /etc/systemd/system/getty@tty1.service.d/autologin.conf
rmdir /etc/systemd/system/getty@tty1.service.d 2>/dev/null || true
cat > "/home/$GUEST_USER/.bash_profile" <<'PROFILE'
[[ -f ~/.bashrc ]] && . ~/.bashrc
PROFILE
chown "$GUEST_USER:$GUEST_USER" "/home/$GUEST_USER/.bash_profile"
# lightdm starts Xorg with -auth /run/lightdm/root/:0, readable by root only;
# the user's session cookie is ~/.Xauthority. Re-install the DISPLAY resolver
# `vm run` sources from its single source, which already prefers whichever
# auth file is readable -- older golden images carry a version that does not.
install -m 644 "$HERE/vmbox-x11.sh" /etc/profile.d/vmbox-x11.sh
systemctl daemon-reload
systemctl enable lightdm.service >/dev/null 2>&1
systemctl set-default graphical.target >/dev/null 2>&1
echo lightdm > "$MARKER"
echo "desktop-lightdm: lightdm.service enabled, autologin ${GUEST_USER} -> i3 (reboot to apply)"

61
vmbox/guest/provision-desktop.sh Executable file → Normal file
View File

@ -49,61 +49,12 @@ I3
chown -R "$GUEST_USER:$GUEST_USER" "/home/$GUEST_USER/.config"
# --- X session discovery for non-login commands ---------------------------
# The image ships a real X/i3 session on tty1 so locker and i3 tests can run.
# But `vm run` executes through `sh -c`, which reads no profile, so those
# The image ships a real X/i3 session so locker and i3 tests can run. But
# `vm run` executes through `sh -c`, which reads no profile, so those
# commands land with no DISPLAY and no XAUTHORITY -- and every X tool then
# fails with "Could not determine i3 socket path" / "cannot open display".
# That looks exactly like "the sandbox has no X", which is wrong and cost a
# real installer run to diagnose.
#
# XAUTHORITY is the awkward half: startx generates /tmp/serverauth.XXXXXXXX
# with a random suffix on every boot, so it cannot be hardcoded. Resolve it
# from the running Xorg process instead, falling back to a glob.
cat > /etc/profile.d/vmbox-x11.sh <<'XENV'
# Point non-login shells at the guest's X session, if one is running.
# Sourced by vmbox's `vm run`; harmless when no X session exists.
if [ -z "${DISPLAY:-}" ]; then
# Wait briefly for the X session rather than reporting "no X".
#
# vmbox now returns control as soon as sshd answers (~11s), which is
# EARLIER than getty@tty1 autologin has run .bash_profile and started
# Xorg. Without this wait the first `vm run` after a boot silently gets
# no DISPLAY and every X tool fails as if the sandbox had no X server --
# the exact failure this file exists to prevent, reintroduced by making
# the rest of the tool faster. Bounded, and skipped entirely once Xorg
# is up, so a guest with no graphical session costs at most this wait.
# Default 0: a plain `vm run` must not pay for a graphical session it is
# not using. Set VMBOX_X_WAIT=<seconds> for X11/i3/locker work.
_vmbox_x_wait="${VMBOX_X_WAIT:-0}"
while [ "$_vmbox_x_wait" -gt 0 ]; do
pgrep -x Xorg >/dev/null 2>&1 && break
sleep 1
_vmbox_x_wait=$((_vmbox_x_wait - 1))
done
unset _vmbox_x_wait
if pgrep -x Xorg >/dev/null 2>&1; then
DISPLAY=":0"
export DISPLAY
fi
fi
if [ -n "${DISPLAY:-}" ] && [ -z "${XAUTHORITY:-}" ]; then
# Prefer the auth file the running Xorg was actually started with.
_vmbox_xauth="$(tr '\0' '\n' < /proc/"$(pgrep -x Xorg | head -1)"/cmdline 2>/dev/null \
| grep -A1 -x -- -auth | tail -1)"
if [ ! -f "${_vmbox_xauth:-}" ]; then
_vmbox_xauth="$(ls -1t /tmp/serverauth.* 2>/dev/null | head -1)"
fi
if [ -f "${_vmbox_xauth:-}" ]; then
XAUTHORITY="$_vmbox_xauth"
export XAUTHORITY
fi
unset _vmbox_xauth
fi
# i3 tools look here when I3SOCK is unset; setting it explicitly avoids a
# second round of X round-trips in tests that shell out repeatedly.
if [ -n "${DISPLAY:-}" ] && [ -z "${I3SOCK:-}" ] && command -v i3 >/dev/null 2>&1; then
I3SOCK="$(i3 --get-socketpath 2>/dev/null || true)"
[ -n "$I3SOCK" ] && export I3SOCK || unset I3SOCK
fi
XENV
chmod 644 /etc/profile.d/vmbox-x11.sh
# real installer run to diagnose. The resolver is its own file, guest/
# vmbox-x11.sh, because desktop-lightdm.sh re-installs it after switching a
# sandbox to lightdm (different auth-file location) -- one source for both.
install -m 644 "$(dirname "${BASH_SOURCE[0]}")/vmbox-x11.sh" /etc/profile.d/vmbox-x11.sh

49
vmbox/guest/shim.sh Normal file
View File

@ -0,0 +1,49 @@
#!/bin/bash
# ============================================================================
# Runs INSIDE a sandbox as root, via `vm shim <name> <tool> [mode]`.
#
# Install a fake <tool> ahead of the real one on PATH (/usr/local/sbin is
# first for root and for systemd services), so scripts that shell out to
# hardware the guest does not have -- openrgb, nvidia-smi, amixer -- can be
# driven through a chosen behaviour instead of "command not found":
#
# exit[:N] return N immediately (default 0)
# hang never return (what openrgb 1.0-2 did on 2026-09-12)
# sleep:S return 0 after S seconds
#
# Every invocation is appended to /var/log/vmbox-shim/<tool>.log as
# "<epoch> <argv>", so the host can assert from the guest what was called,
# with which arguments, and in what order relative to other shims.
# ============================================================================
set -euo pipefail
TOOL="${1:?usage: shim.sh <tool> [exit[:N]|hang|sleep:S]}"
MODE="${2:-exit}"
# Overridable for the host-side bats tests, which must not write to /usr.
readonly SHIM_DIR="${VMBOX_SHIM_DIR:-/usr/local/sbin}"
readonly LOG_DIR="${VMBOX_SHIM_LOG_DIR:-/var/log/vmbox-shim}"
case "$TOOL" in
*/*|'') echo "shim: tool must be a bare command name, got '$TOOL'" >&2; exit 1 ;;
esac
behaviour=""
case "$MODE" in
exit) behaviour='exit 0' ;;
exit:*) behaviour="exit ${MODE#exit:}" ;;
hang) behaviour='exec sleep infinity' ;;
sleep:*) behaviour="sleep ${MODE#sleep:}; exit 0" ;;
*) echo "shim: unknown mode '$MODE' (exit[:N] | hang | sleep:S)" >&2; exit 1 ;;
esac
install -d -m 755 "$LOG_DIR" "$SHIM_DIR"
cat > "$SHIM_DIR/$TOOL" <<SHIM
#!/bin/bash
# vmbox shim for ${TOOL} (mode: ${MODE}) -- installed by \`vm shim\`.
printf '%(%s)T %s\\n' -1 "\$*" >> "${LOG_DIR}/${TOOL}.log"
${behaviour}
SHIM
chmod 755 "$SHIM_DIR/$TOOL"
: > "$LOG_DIR/$TOOL.log"
echo "shim: $SHIM_DIR/$TOOL installed (mode: $MODE), log: $LOG_DIR/$TOOL.log"

59
vmbox/guest/vmbox-x11.sh Normal file
View File

@ -0,0 +1,59 @@
#!/bin/sh
# Installed to /etc/profile.d/vmbox-x11.sh by provision-desktop.sh (golden
# image) and re-installed by desktop-lightdm.sh (per-sandbox lightdm switch).
# Sourced by `vm run`; POSIX sh, because /etc/profile.d may be read by dash.
#
# Point non-login shells at the guest's X session, if one is running.
# Sourced by vmbox's `vm run`; harmless when no X session exists.
if [ -z "${DISPLAY:-}" ]; then
# Wait briefly for the X session rather than reporting "no X".
#
# vmbox now returns control as soon as sshd answers (~11s), which is
# EARLIER than getty@tty1 autologin has run .bash_profile and started
# Xorg. Without this wait the first `vm run` after a boot silently gets
# no DISPLAY and every X tool fails as if the sandbox had no X server --
# the exact failure this file exists to prevent, reintroduced by making
# the rest of the tool faster. Bounded, and skipped entirely once Xorg
# is up, so a guest with no graphical session costs at most this wait.
# Default 0: a plain `vm run` must not pay for a graphical session it is
# not using. Set VMBOX_X_WAIT=<seconds> for X11/i3/locker work.
_vmbox_x_wait="${VMBOX_X_WAIT:-0}"
while [ "$_vmbox_x_wait" -gt 0 ]; do
pgrep -x Xorg >/dev/null 2>&1 && break
sleep 1
_vmbox_x_wait=$((_vmbox_x_wait - 1))
done
unset _vmbox_x_wait
if pgrep -x Xorg >/dev/null 2>&1; then
DISPLAY=":0"
export DISPLAY
fi
fi
if [ -n "${DISPLAY:-}" ] && [ -z "${XAUTHORITY:-}" ]; then
# Prefer the auth file the running Xorg was actually started with. Test
# READABILITY, not existence: under lightdm that is /run/lightdm/root/:0,
# root-only, and the user's cookie is ~/.Xauthority instead. The startx
# serverauth glob is the last resort for the golden image's tty1 session.
_vmbox_xauth="$(tr '\0' '\n' < /proc/"$(pgrep -x Xorg | head -1)"/cmdline 2>/dev/null \
| grep -A1 -x -- -auth | tail -1)"
if [ ! -r "${_vmbox_xauth:-}" ]; then
_vmbox_xauth="$HOME/.Xauthority"
fi
if [ ! -r "${_vmbox_xauth:-}" ]; then
for _vmbox_cand in /tmp/serverauth.*; do
[ -r "$_vmbox_cand" ] && _vmbox_xauth="$_vmbox_cand"
done
unset _vmbox_cand
fi
if [ -r "${_vmbox_xauth:-}" ]; then
XAUTHORITY="$_vmbox_xauth"
export XAUTHORITY
fi
unset _vmbox_xauth
fi
# i3 tools look here when I3SOCK is unset; setting it explicitly avoids a
# second round of X round-trips in tests that shell out repeatedly.
if [ -n "${DISPLAY:-}" ] && [ -z "${I3SOCK:-}" ] && command -v i3 >/dev/null 2>&1; then
I3SOCK="$(i3 --get-socketpath 2>/dev/null || true)"
if [ -n "$I3SOCK" ]; then export I3SOCK; else unset I3SOCK; fi
fi

View File

@ -114,9 +114,11 @@ base_provision() {
phase_begin "Provisioning guest (installs Xorg/i3 + dev tools)"
# provision.sh sources a sibling (provision-desktop.sh) by path, so BOTH
# must land in the same directory in the guest -- copying only the entry
# must land in the same directory in the guest (and provision-desktop.sh
# installs vmbox-x11.sh from beside itself) -- copying only the entry
# point produces a build that dies partway with "No such file or directory".
_base_scp "$VMBOX_GUEST_DIR/provision.sh" "/tmp/provision.sh"
_base_scp "$VMBOX_GUEST_DIR/vmbox-x11.sh" "/tmp/vmbox-x11.sh"
_base_scp "$VMBOX_GUEST_DIR/provision-desktop.sh" "/tmp/provision-desktop.sh"
_base_ssh "chmod +x /tmp/provision.sh /tmp/provision-desktop.sh && sudo /tmp/provision.sh '$VMBOX_GUEST_USER'" ||
die "provisioning failed -- see $serial"

88
vmbox/lib/desktop.sh Normal file
View File

@ -0,0 +1,88 @@
#!/bin/bash
# ============================================================================
# vmbox: make a sandbox look like the host where the base image cannot.
#
# vm lightdm <name> lightdm autologin -> i3, tty1 a plain getty
# vm shim <name> <tool> [mode] fake a hardware tool (openrgb, nvidia-smi)
#
# Both mutate a live overlay and are undone by `vm reset`. Neither touches the
# golden image: rebuilding it would silently corrupt every existing sandbox.
# ============================================================================
# shellcheck source=common.sh
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
source "$(dirname "${BASH_SOURCE[0]}")/ssh.sh"
source "$(dirname "${BASH_SOURCE[0]}")/launch.sh"
readonly DESKTOP_REBOOT_SSH_TIMEOUT="${VMBOX_DESKTOP_REBOOT_SSH_TIMEOUT:-150}"
_desktop_ensure_running() {
local name="$1"
if ! vm_is_running "$name"; then
launch_vm "$name" >/dev/null
vm_wait_ssh "$name" 150 || die "sandbox '$name' did not become reachable"
fi
}
# Copy a guest/ script (plus any siblings it needs) in and run it as root.
_desktop_run_guest_script() {
local name="$1" script="$2"; shift 2
local f
for f in "$script" "$@"; do
vm_scp_to "$name" "$VMBOX_GUEST_DIR/$f" "/tmp/$f" ||
die "could not copy $f into '$name'"
done
vm_ssh_exec "$name" "chmod +x /tmp/$script && sudo /tmp/$script $VMBOX_GUEST_USER"
}
# vm lightdm <name>
cmd_lightdm() {
local name
name="$(validate_vm_name "${1:-}")"
require_vm "$name"
_desktop_ensure_running "$name"
log "Switching '$name' to lightdm autologin -> i3 (host topology)"
_desktop_run_guest_script "$name" desktop-lightdm.sh vmbox-x11.sh ||
die "lightdm switch failed in '$name'"
# The switch only takes effect on a fresh boot: lightdm must own the seat
# from the start, and the startx session on tty1 must never have run.
# -no-reboot makes a guest reboot exit qemu, so relaunch it ourselves.
log "Rebooting '$name' to bring lightdm up"
vm_ssh_exec "$name" "sudo systemctl reboot" >/dev/null 2>&1 || true
local waited=0
while vm_is_running "$name" && (( waited < 60 )); do
sleep 1; waited=$(( waited + 1 ))
done
vm_is_running "$name" && die "'$name' did not go down for the reboot"
launch_vm "$name" >/dev/null
vm_wait_ssh "$name" "$DESKTOP_REBOOT_SSH_TIMEOUT" ||
die "'$name' did not come back after the reboot"
# Prove the topology from the guest, not from the exit code: the unit is
# active AND an Xorg it owns is up -- that is what a lockdown script sees.
local state
state="$(vm_ssh_exec "$name" "export VMBOX_X_WAIT=45; . /etc/profile.d/vmbox-x11.sh; \
printf '%s %s %s' \"\$(systemctl is-active lightdm.service)\" \
\"\$(systemctl is-enabled getty@tty1.service 2>/dev/null || echo -)\" \"\${DISPLAY:-none}\"" 2>/dev/null)"
[[ "$state" == "active "*" :0" ]] ||
die "lightdm did not come up cleanly in '$name' (lightdm/getty@tty1/DISPLAY: $state)"
ok "'$name': lightdm.service active, autologin i3 session on ${state##* }"
}
# vm shim <name> <tool> [exit[:N]|hang|sleep:S]
cmd_shim() {
local name tool mode
name="$(validate_vm_name "${1:-}")"
tool="${2:-}"; mode="${3:-exit}"
[[ -n "$tool" ]] || die "usage: vm shim <name> <tool> [exit[:N]|hang|sleep:S]"
require_vm "$name"
_desktop_ensure_running "$name"
vm_scp_to "$name" "$VMBOX_GUEST_DIR/shim.sh" "/tmp/shim.sh" ||
die "could not copy shim.sh into '$name'"
vm_ssh_exec "$name" "chmod +x /tmp/shim.sh && sudo /tmp/shim.sh '$tool' '$mode'" ||
die "shim install failed in '$name'"
ok "'$name': $tool is now a shim ($mode); calls logged to /var/log/vmbox-shim/$tool.log"
}

View File

@ -389,3 +389,41 @@ _verdict_case() {
[ "$status" -eq 6 ]
[[ "$output" == *"unrecognised"* ]]
}
# ---------------------------------------------------------------------------
# guest/shim.sh -- fake hardware tools (runs on the host against a temp PATH)
# ---------------------------------------------------------------------------
_shim() {
VMBOX_SHIM_DIR="$VMBOX_TEST_HOME/sbin" VMBOX_SHIM_LOG_DIR="$VMBOX_TEST_HOME/shimlog" \
bash "$REPO_ROOT/guest/shim.sh" "$@"
}
@test "shim: exit mode returns 0 and logs the argv with an epoch" {
run _shim openrgb exit
[ "$status" -eq 0 ]
run "$VMBOX_TEST_HOME/sbin/openrgb" --mode static --color 000000
[ "$status" -eq 0 ]
run cat "$VMBOX_TEST_HOME/shimlog/openrgb.log"
[[ "$output" =~ ^[0-9]{9,}\ --mode\ static\ --color\ 000000$ ]]
}
@test "shim: exit:N returns N" {
_shim nvidia-smi exit:3
run "$VMBOX_TEST_HOME/sbin/nvidia-smi" -pl 100
[ "$status" -eq 3 ]
}
@test "shim: hang mode never returns (the 2026-09-12 openrgb)" {
_shim openrgb hang
run timeout 1 "$VMBOX_TEST_HOME/sbin/openrgb"
[ "$status" -eq 124 ]
run cat "$VMBOX_TEST_HOME/shimlog/openrgb.log"
[ "${#lines[@]}" -eq 1 ]
}
@test "shim: an unknown mode or a path is refused" {
run _shim openrgb explode
[ "$status" -eq 1 ]
run _shim /usr/bin/openrgb exit
[ "$status" -eq 1 ]
}