From f3f8f67a62eccb7942dacb7c6cb32afa76a3c8af Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Wed, 28 Oct 2026 16:16:32 +0100 Subject: [PATCH] tools: a remote display script, Xvfb plus x11vnc over an SSH tunnel Over SSH with X11 forwarding the app does not appear: no usable Vulkan device means GTK cannot create a swapchain, and `--renderer cairo` fixes that half only -- the plot is a GtkGLArea and asks for its own GL context, so indirect GLX is the second wall. A virtual X server on the machine that runs the app sidesteps both, because the GL context then lives next door to the samples. `tools/udpscope-remote.sh` starts Xvfb on a free display, runs the app there with `--renderer cairo`, and serves the screen through x11vnc bound to localhost only -- the SSH tunnel is the authentication, so `-nopw` is deliberate and `-localhost` is what keeps a mistake from being a remotely reachable screen. It prints the tunnel and viewer commands rather than assuming a client, mentions the websockify/noVNC route for a browser, and takes the X and VNC servers with it when the app exits. `--no-vnc` leaves just the virtual display, for another viewer; the preflight names the install command instead of a bare "command not found". Verified: the preflight refuses with exit 1 and the install line when x11vnc is absent; with `--no-vnc` the app runs against the virtual display and reads `buffer allocated: 4 channels` with no Vulkan warning and no critical, and no Xvfb is left behind when the timeout ends it. That run also exercises what the earlier log showed by omission: nothing is enabled, so a working app plots nothing. The README's Limitations gains the bullet a person actually needs -- the exact warning string, that both halves need the software preset, the script itself, and that a window which appears but stays empty usually means no signal is enabled. --- README.md | 10 +++ tools/udpscope-remote.sh | 135 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100755 tools/udpscope-remote.sh diff --git a/README.md b/README.md index 25a2a25..64bbd6b 100644 --- a/README.md +++ b/README.md @@ -2893,6 +2893,16 @@ silence timeout and then resumes, which is the EMI case end to end. ## Limitations +- **A remote display needs the software renderer, and the plot is empty until a signal is + enabled.** Over SSH with X11 forwarding there is no usable Vulkan device: GTK cannot create a + swapchain (`vkCreateSwapchainKHR … VK_ERROR_INITIALIZATION_FAILED`) and the window never appears + even though the stream connects and its CONFIG parses. `--renderer cairo` selects the software + preset for **both** halves — GTK's renderer and the plot's own GL context, which is a + `GtkGLArea` and asks for one regardless of what GSK draws with — and + `tools/udpscope-remote.sh` runs the app against a virtual X server with a VNC view carried over + an SSH tunnel, avoiding X11 forwarding altogether. If a window does appear but stays empty, check + that a **signal is enabled**: signals start disabled, so a session with none switched on plots + nothing and prints no `buffer allocated:` line, by design rather than as a fault. - **Multi-element (vector/matrix) signals are acquired with a zero-order hold.** `udps_client` decodes exactly one *sample* of `num_rows x num_cols` values per DATA packet for such a signal, regardless of the publish mode, so it cannot be diff --git a/tools/udpscope-remote.sh b/tools/udpscope-remote.sh new file mode 100755 index 0000000..a9857c5 --- /dev/null +++ b/tools/udpscope-remote.sh @@ -0,0 +1,135 @@ +#!/bin/sh +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2026 Martino Ferrari +# +# Run udpscope on a headless machine and look at it over an SSH tunnel. +# +# Why this exists: over SSH with X11 forwarding there is no usable Vulkan device, so GTK cannot +# create a swapchain and the window never appears; `--renderer cairo` fixes GTK's half, but the +# plot is a GtkGLArea and indirect GLX is the second wall. A virtual X server on the machine that +# actually runs the app sidesteps both -- the GL context lives next door to the samples -- and VNC +# carries the picture back through a tunnel. `ssh -X` is not needed and not used. +# +# The picture is rendered by Xvfb's software path, so the app must be told to use its software +# preset too (`--renderer cairo`), otherwise GTK reaches for Vulkan inside Xvfb and warns exactly +# as it does over a forwarded display. +# +# x11vnc binds to localhost only and runs without a password *by design*: the SSH tunnel is the +# authentication. Never replace that with `-listen 0.0.0.0`, and never open the VNC port on the +# host's firewall. `-localhost` exists here so that a mistake is not a remotely reachable screen. +# +# Usage: +# tools/udpscope-remote.sh [--size WxH] [--port N] [--app PATH] [--no-vnc] [-- udpscope args...] +# +# --size WxH virtual screen (default 1600x1000) +# --port N VNC port on localhost (default 5900) +# --app PATH the binary to run (default build/udpscope) +# --no-vnc start the X server and the app only; for another viewer, or to test the server +# -- everything after this is passed to udpscope unchanged +# +# What it prints when it is up: +# * the ssh command to run on *your* machine, which forwards the VNC port, +# * the viewer command (any VNC client), and +# * optionally how to get a browser instead (websockify + noVNC, if installed). +# +# Exits when udpscope exits, or on Ctrl-C, taking the X server and the VNC server with it. +set -eu + +here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) +root=$(dirname -- "$here") + +size=1600x1000 +port=5900 +app=${UDPSCOPE_REMOTE_APP:-$root/build/udpscope} +vnc=1 +app_args= + +while [ $# -gt 0 ]; do + case $1 in + --size) size=$2; shift 2 ;; + --port) port=$2; shift 2 ;; + --app) app=$2; shift 2 ;; + --no-vnc) vnc=0; shift ;; + --help|-h) sed -n '7,37p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + --) shift; app_args=$*; break ;; + *) echo "udpscope-remote: unknown option $1 (use -- to pass options to udpscope)" >&2; exit 2 ;; + esac +done + +# Preflight, with the fix in the message rather than a bare "command not found". +need() { + command -v "$1" >/dev/null 2>&1 || { + echo "udpscope-remote: $1 is required. $2" >&2 + exit 1 + } +} +need Xvfb "Install Xvfb (Arch: pacman -S xorg-server-xvfb; Debian: apt install xvfb)." +if [ "$vnc" = 1 ]; then + need x11vnc "Install x11vnc (Arch: pacman -S x11vnc; Debian: apt install x11vnc), or pass --no-vnc." +fi +[ -x "$app" ] || { echo "udpscope-remote: $app is not executable; build it first or pass --app" >&2; exit 1; } + +# A free display: probe the ones a desktop is unlikely to be using and take the first that answers +# no. `xdpyinfo` failing is the test -- a stale lock file does not matter, only a live server does. +display= +n=90 +while [ "$n" -le 120 ]; do + if ! xdpyinfo -display ":$n" >/dev/null 2>&1; then display=":$n"; break; fi + n=$((n + 1)) +done +[ -n "$display" ] || { echo "udpscope-remote: no free display in :90..:120" >&2; exit 1; } + +xvfb_pid= +app_pid= +vnc_pid= +cleanup() { + [ -n "${vnc_pid:-}" ] && kill "$vnc_pid" 2>/dev/null || true + [ -n "${app_pid:-}" ] && kill "$app_pid" 2>/dev/null || true + [ -n "${xvfb_pid:-}" ] && kill "$xvfb_pid" 2>/dev/null || true + wait 2>/dev/null || true +} +trap cleanup EXIT INT TERM + +# `-nolisten tcp` too: the virtual server is for this machine's sockets only. +Xvfb "$display" -screen 0 "${size}x24" -nolisten tcp >/dev/null 2>&1 & +xvfb_pid=$! +for _ in 1 2 3 4 5 6 7 8 9 10; do + xdpyinfo -display "$display" >/dev/null 2>&1 && break + sleep 0.2 +done +xdpyinfo -display "$display" >/dev/null 2>&1 || { echo "udpscope-remote: Xvfb did not come up on $display" >&2; exit 1; } + +export DISPLAY="$display" +# The software preset, as one option: GTK's renderer *and* the GL-area path (see --renderer's help). +[ "$vnc" = 1 ] && echo "udpscope-remote: screen $display ${size}x24, vnc on localhost:$port" + +# shellcheck disable=SC2086 +"$app" --renderer cairo $app_args & +app_pid=$! + +if [ "$vnc" = 1 ]; then + x11vnc -display "$display" -localhost -nopw -forever -shared -quiet -rfbport "$port" & + vnc_pid=$! + sleep 1 + cat < + +then, in another terminal on your machine: + + vncviewer localhost:$port # or any VNC client + +A browser instead of a VNC client, if websockify and noVNC are installed here: + + websockify 6080 localhost:$port & + # ...and open http://localhost:6080/vnc.html through a second tunnel: + # ssh -N -L 6080:localhost:6080 $(id -un)@ + +Ctrl-C here, or quitting udpscope, closes the app and both servers. +EOF +fi + +# The app owns the run: when it exits, so does everything else (the trap). +wait "$app_pid" || true