#!/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