From eb08cf76fc2d81341ab1d9b064432472ed2e2d81 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 22:18:35 +0000 Subject: [PATCH] fix(rpi): stop the kiosk login loop by pinning the vc4 display device On a Pi, dtoverlay=vc4-kms-v3d registers two DRM devices: vc4, which owns the HDMI connectors, and v3d, a render-only core with no outputs. Their /dev/dri/card* numbering is not stable, and when v3d lands on card0 Xorg's modesetting driver binds to it, finds no screens and exits immediately. The console is unaffected - fbcon is bound to vc4 - so the machine boots, prints the banner, and then loops: startx was exec'd from .bash_profile, so a failed session ended the login shell, agetty logged the user straight back in, and the only explanation went to a log file inside a session that never lasted long enough to read it. - Add the vc4 OutputClass/PrimaryGPU rule to the Pi overlay, matching on the kernel driver name rather than a card number. Raspberry Pi OS ships the same rule; Arch Linux ARM does not. - Run startx as a child of the login shell instead of exec'ing it. A session that lived logs out as before; three failures inside a few seconds stop the retries and leave a shell on tty1 with the tail of the session and kiosk logs on screen. - Fall back to software rendering when the editor exits immediately three times, and detect a usable DRM node instead of just /dev/dri existing. - Assert the vc4 rule made it into the image at build time, and document the console fallback. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Mej775L1dYS9Hk6JtvcHCJ --- README.md | 12 ++++- rootfs-common/etc/skel/.bash_profile | 52 +++++++++++++++++-- rootfs-common/usr/local/bin/vscodeos-kiosk | 38 ++++++++++++-- rpi/build-image.sh | 7 +++ .../etc/X11/xorg.conf.d/20-vscodeos-vc4.conf | 22 ++++++++ 5 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 rpi/overlay/etc/X11/xorg.conf.d/20-vscodeos-vc4.conf diff --git a/README.md b/README.md index 264b316..b3f52cf 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,15 @@ Recovery is by design, not by accident, and differs by target: `systemd.unit=multi-user.target` to `cmdline.txt` on the FAT boot partition. It is plain FAT, so Windows and macOS can edit it too. Keep it to one line. +A session that cannot start is the one case where the kiosk gets out of your +way: if `startx` dies three times in a row within seconds, tty1 stops retrying +and leaves an ordinary shell with the reason on screen — the tail of +`~/.local/share/vscodeos/xorg-session.log` and, when the X server did come up, +of `~/.local/share/vscodeos/kiosk.log`. The full X server log is in +`~/.local/share/xorg/Xorg.0.log`. Without that stop, an auto-logged-in tty1 +answers a broken session by logging straight back in, which looks like the +machine looping on its login banner. + To relax the kiosk permanently, edit `/etc/default/vscodeos`: ```bash @@ -317,7 +326,8 @@ rpi/ Raspberry Pi only build-image.sh assembles the flashable disk image packages.aarch64 packages for the Pi image boot/config.txt, cmdline.txt Raspberry Pi firmware configuration - overlay/ fstab, ALARM mirrors, zram, and the + overlay/ fstab, ALARM mirrors, zram, the vc4 + PrimaryGPU rule for Xorg, and the first-boot root filesystem expansion scripts/ diff --git a/rootfs-common/etc/skel/.bash_profile b/rootfs-common/etc/skel/.bash_profile index 4f554bb..073c745 100644 --- a/rootfs-common/etc/skel/.bash_profile +++ b/rootfs-common/etc/skel/.bash_profile @@ -5,7 +5,53 @@ [[ -f ~/.bashrc ]] && . ~/.bashrc -if [[ -z "${DISPLAY}" && "${XDG_VTNR}" == "1" ]]; then - mkdir -p ~/.local/share/vscodeos - exec startx -- -nolisten tcp vt1 &> ~/.local/share/vscodeos/xorg-session.log +# XDG_VTNR comes from pam_systemd and is the reliable answer, but it is empty +# if the login was not tracked by logind - fall back to the tty name so the +# kiosk still starts instead of silently leaving a bare shell. +if [[ -z "${DISPLAY:-}" ]] && + { [[ "${XDG_VTNR:-}" == "1" ]] || [[ "$(tty 2>/dev/null)" == "/dev/tty1" ]]; }; then + + vscodeos_session_dir="${HOME}/.local/share/vscodeos" + vscodeos_session_log="${vscodeos_session_dir}/xorg-session.log" + mkdir -p "${vscodeos_session_dir}" + + # startx is *not* exec'd. If it were, a session that fails to come up would + # end the login shell, agetty would log the user straight back in, and the + # machine would sit in a login loop with the reason buried in a log file no + # one can reach. Running it as a child lets us tell "the user ended a real + # session" (start another one, the kiosk is supposed to come back) apart + # from "X died on its face" (stop, and show why). + vscodeos_failures=0 + while :; do + vscodeos_started=${SECONDS} + startx -- -nolisten tcp vt1 >"${vscodeos_session_log}" 2>&1 + vscodeos_rc=$? + vscodeos_ran=$(( SECONDS - vscodeos_started )) + + if (( vscodeos_ran >= 15 )); then + # A session that lived is a session that worked. Log out and let + # agetty start a fresh one, exactly as before. + exit + fi + + (( vscodeos_failures++ )) + (( vscodeos_failures >= 3 )) && break + sleep 2 + done + + printf '\n\e[38;5;203mThe VS Code OS session could not start.\e[0m\n' + printf 'startx exited with status %s after %ss, %s times in a row.\n\n' \ + "${vscodeos_rc}" "${vscodeos_ran}" "${vscodeos_failures}" + printf -- '--- %s (last 20 lines) ---\n' "${vscodeos_session_log}" + tail -n 20 "${vscodeos_session_log}" 2>/dev/null + if [[ -s "${HOME}/.local/share/vscodeos/kiosk.log" ]]; then + printf -- '\n--- kiosk.log (last 20 lines) ---\n' + tail -n 20 "${HOME}/.local/share/vscodeos/kiosk.log" + fi + printf '\nThe X server keeps its own log in ~/.local/share/xorg/Xorg.0.log\n' + printf 'Retry the session with: startx -- -nolisten tcp vt1\n\n' + + unset vscodeos_session_dir vscodeos_session_log vscodeos_failures \ + vscodeos_started vscodeos_rc vscodeos_ran + # Falling through leaves an interactive shell on tty1 instead of looping. fi diff --git a/rootfs-common/usr/local/bin/vscodeos-kiosk b/rootfs-common/usr/local/bin/vscodeos-kiosk index e54a28b..ff72bf2 100755 --- a/rootfs-common/usr/local/bin/vscodeos-kiosk +++ b/rootfs-common/usr/local/bin/vscodeos-kiosk @@ -67,10 +67,19 @@ declare -a flags=( ) # Machines without a render node (most VMs, some servers) hang or fall over on -# GPU init, so ask Electron for software rendering there. -if [[ ! -d /dev/dri ]]; then - log "no /dev/dri present - falling back to software rendering" +# GPU init, so ask Electron for software rendering there. `-d /dev/dri` is not +# enough: the directory exists on a Raspberry Pi even when the only thing in it +# is a display node with no 3D behind it. +software_rendering=0 +use_software_rendering() { + (( software_rendering )) && return 0 + software_rendering=1 flags+=("--disable-gpu" "--disable-gpu-compositing") +} + +if ! compgen -G '/dev/dri/render*' >/dev/null && ! compgen -G '/dev/dri/card*' >/dev/null; then + log "no DRM device in /dev/dri - falling back to software rendering" + use_software_rendering fi # Word-split the admin-provided flags on purpose: /etc/default/vscodeos is a @@ -81,8 +90,16 @@ if [[ -n "${VSCODEOS_CODE_FLAGS}" ]]; then fi if [[ ! -x "${CODE_BIN}" ]]; then - log "FATAL: ${CODE_BIN} is missing - dropping to a terminal" - exec xterm -fa Monospace -fs 12 -e "echo 'VS Code is not installed at ${CODE_BIN}'; bash" + log "FATAL: ${CODE_BIN} is missing" + if command -v xterm >/dev/null 2>&1; then + log "dropping to a terminal" + exec xterm -fa Monospace -fs 12 -e "echo 'VS Code is not installed at ${CODE_BIN}'; bash" + fi + # No terminal to fall back to either. Exiting ends the X session; the tty1 + # bootstrap notices the quick failure and leaves a console with this log on + # screen, which beats respawning an empty X server forever. + log "xterm is not installed either - ending the session" + exit 1 fi # --- supervisor loop -------------------------------------------------------- @@ -100,6 +117,7 @@ trap cleanup EXIT trap '' HUP INT QUIT backoff=1 +quick_exits=0 while :; do log "launching VS Code: ${CODE_BIN} ${flags[*]} ${VSCODEOS_WORKSPACE}" start=${SECONDS} @@ -120,10 +138,20 @@ while :; do # A crash loop (repeated exits within a few seconds) backs off instead of # spinning the CPU; a normal long-lived session restarts instantly. if (( ran < 5 )); then + (( quick_exits++ )) + # An editor that will not stay up is usually Electron falling over on + # GPU init - the Pi's v3d driver is a common way to get there. Retry + # without hardware acceleration before settling into the back-off: a + # slow desktop is worth more than a blank screen. + if (( quick_exits == 3 )) && (( ! software_rendering )); then + log "three quick exits - retrying with software rendering" + use_software_rendering + fi log "restarting in ${backoff}s" sleep "${backoff}" (( backoff < 30 )) && backoff=$(( backoff * 2 )) else backoff=1 + quick_exits=0 fi done diff --git a/rpi/build-image.sh b/rpi/build-image.sh index b275812..95d0e7a 100755 --- a/rpi/build-image.sh +++ b/rpi/build-image.sh @@ -446,6 +446,13 @@ done grep -q "${PARTUUID_PREFIX}-01" "${ROOT_MNT}/etc/fstab" || die "fstab does not reference the boot partition PARTUUID ${PARTUUID_PREFIX}-01" +# Without this rule Xorg can bind to the render-only v3d device, find no +# outputs and exit - which on an auto-logged-in tty1 is an endless login loop. +# The console still works in that state, so nothing else in this build would +# catch it; check the file is there instead of finding out on the hardware. +[[ -f "${ROOT_MNT}/etc/X11/xorg.conf.d/20-vscodeos-vc4.conf" ]] || + die "the vc4 PrimaryGPU rule is missing from the image - the kiosk would not start" + # The shared sudoers drop-in grants the live medium password-less sudo, which # the x86 installer replaces at install time. A flashed Pi image has no # installer, so apply the same tightening here. diff --git a/rpi/overlay/etc/X11/xorg.conf.d/20-vscodeos-vc4.conf b/rpi/overlay/etc/X11/xorg.conf.d/20-vscodeos-vc4.conf new file mode 100644 index 0000000..c3ac239 --- /dev/null +++ b/rpi/overlay/etc/X11/xorg.conf.d/20-vscodeos-vc4.conf @@ -0,0 +1,22 @@ +# VS Code OS on Raspberry Pi: tell Xorg which DRM device drives the screen. +# +# `dtoverlay=vc4-kms-v3d` registers two DRM devices, not one: +# +# vc4 - the display controller; this is the one with the HDMI connectors +# v3d - the 3D core; a render-only device with no outputs at all +# +# Their /dev/dri/card* numbering is not stable, and when v3d comes up as card0 +# the modesetting driver picks it, finds no connectors and exits with +# "no screens found (EE)". The console stays readable (fbcon is bound to vc4), +# so the machine looks fine right up until the graphical session dies - and +# because tty1 is auto-logged in, dying immediately is an endless login loop. +# +# Matching on the kernel driver name instead of a card number pins the display +# device regardless of enumeration order. Raspberry Pi OS ships the same rule; +# Arch Linux ARM does not, which is why it lives here. +Section "OutputClass" + Identifier "vc4-kms" + MatchDriver "vc4" + Driver "modesetting" + Option "PrimaryGPU" "true" +EndSection