From 00bb6706fd35b44490a84ae16bc64083b2369899 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 13 Sep 2026 10:51:10 +0200 Subject: [PATCH] =?UTF-8?q?vmbox:=20`vm=20lightdm`=20and=20`vm=20shim`=20?= =?UTF-8?q?=E2=80=94=20the=20host's=20login=20topology=20and=20fake=20hard?= =?UTF-8?q?ware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `: 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 [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/.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) Claude-Session: https://claude.ai/code/session_015mLnUeoJ3mUwGRTowTUH6d --- vmbox/README.md | 45 ++++++++++++---- vmbox/bin/vm | 6 +++ vmbox/guest/desktop-lightdm.sh | 69 +++++++++++++++++++++++++ vmbox/guest/provision-desktop.sh | 61 +++------------------- vmbox/guest/shim.sh | 49 ++++++++++++++++++ vmbox/guest/vmbox-x11.sh | 59 +++++++++++++++++++++ vmbox/lib/base.sh | 4 +- vmbox/lib/desktop.sh | 88 ++++++++++++++++++++++++++++++++ vmbox/tests/test_vmbox.bats | 38 ++++++++++++++ 9 files changed, 354 insertions(+), 65 deletions(-) create mode 100644 vmbox/guest/desktop-lightdm.sh mode change 100755 => 100644 vmbox/guest/provision-desktop.sh create mode 100644 vmbox/guest/shim.sh create mode 100644 vmbox/guest/vmbox-x11.sh create mode 100644 vmbox/lib/desktop.sh diff --git a/vmbox/README.md b/vmbox/README.md index c1957c0..93b6192 100644 --- a/vmbox/README.md +++ b/vmbox/README.md @@ -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 ` | Run a command, print the verdict | | `vm ssh ` | Interactive shell (starts the VM if needed) | | `vm screenshot [out.png]` | Capture the screen (locker/X11 tests) — verified: real 1280x800 PNG of the guest's i3 session, no viewer installed | +| `vm lightdm ` | 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 [exit[:N]\|hang\|sleep:S]` | Put a fake `` ahead of the real one on PATH (`/usr/local/sbin`). Every call is logged with an epoch to `/var/log/vmbox-shim/.log`, so the host can assert what ran and in what order. `hang` reproduces a tool that never returns | | `vm reset ` | Wipe back to pristine | | `vm rm ` / `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 diff --git a/vmbox/bin/vm b/vmbox/bin/vm index 4cc8ca7..94f4aa4 100755 --- a/vmbox/bin/vm +++ b/vmbox/bin/vm @@ -28,6 +28,10 @@ Usage: vm [args] repo [--worktree] Copy a host repo in (clone=HEAD, --worktree=uncommitted) screenshot [out.png] Capture the guest screen (for locker tests) + lightdm Switch the sandbox to lightdm autologin -> i3, + tty1 a plain getty (the host's login topology) + shim [mode] Fake a hardware tool on PATH: exit[:N] | hang | + sleep:S; calls logged in /var/log/vmbox-shim/ reset Wipe back to pristine (sub-second) rm 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 "$@" ;; diff --git a/vmbox/guest/desktop-lightdm.sh b/vmbox/guest/desktop-lightdm.sh new file mode 100644 index 0000000..2696b8a --- /dev/null +++ b/vmbox/guest/desktop-lightdm.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# ============================================================================ +# Runs INSIDE a sandbox as root, via `vm lightdm `. +# +# 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 </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)" diff --git a/vmbox/guest/provision-desktop.sh b/vmbox/guest/provision-desktop.sh old mode 100755 new mode 100644 index 2eb217a..9a83516 --- a/vmbox/guest/provision-desktop.sh +++ b/vmbox/guest/provision-desktop.sh @@ -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= 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 diff --git a/vmbox/guest/shim.sh b/vmbox/guest/shim.sh new file mode 100644 index 0000000..d243ef7 --- /dev/null +++ b/vmbox/guest/shim.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# ============================================================================ +# Runs INSIDE a sandbox as root, via `vm shim [mode]`. +# +# Install a fake 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/.log as +# " ", 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 [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" <> "${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" diff --git a/vmbox/guest/vmbox-x11.sh b/vmbox/guest/vmbox-x11.sh new file mode 100644 index 0000000..a589ce7 --- /dev/null +++ b/vmbox/guest/vmbox-x11.sh @@ -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= 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 diff --git a/vmbox/lib/base.sh b/vmbox/lib/base.sh index cf53c33..bd1a786 100644 --- a/vmbox/lib/base.sh +++ b/vmbox/lib/base.sh @@ -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" diff --git a/vmbox/lib/desktop.sh b/vmbox/lib/desktop.sh new file mode 100644 index 0000000..59dd3ff --- /dev/null +++ b/vmbox/lib/desktop.sh @@ -0,0 +1,88 @@ +#!/bin/bash +# ============================================================================ +# vmbox: make a sandbox look like the host where the base image cannot. +# +# vm lightdm lightdm autologin -> i3, tty1 a plain getty +# vm shim [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 +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 [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 [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" +} diff --git a/vmbox/tests/test_vmbox.bats b/vmbox/tests/test_vmbox.bats index c20513e..fab1840 100644 --- a/vmbox/tests/test_vmbox.bats +++ b/vmbox/tests/test_vmbox.bats @@ -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 ] +}