Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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/
Expand Down
52 changes: 49 additions & 3 deletions rootfs-common/etc/skel/.bash_profile
Original file line number Diff line number Diff line change
Expand Up @@ -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
38 changes: 33 additions & 5 deletions rootfs-common/usr/local/bin/vscodeos-kiosk
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 --------------------------------------------------------
Expand All @@ -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}
Expand All @@ -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
7 changes: 7 additions & 0 deletions rpi/build-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
22 changes: 22 additions & 0 deletions rpi/overlay/etc/X11/xorg.conf.d/20-vscodeos-vc4.conf
Original file line number Diff line number Diff line change
@@ -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