Files
DeeJanuz--frametop/session/vnc-bridge.sh
T
DeeJanuzandClaude Opus 5.5 3e7248a04e Remote desktop: connect FreeRDP only while a VNC viewer is connected
vnc-bridge.sh kept FreeRDP connected to krdpserver from the moment remote desktop
started, so krdp captured and H.264-encoded every KWin redraw in software (openh264)
with nobody watching: krdpserver 55-78% of a core, xfreerdp 16-27%, Xvnc 6-11%, with 0
clients on :5900. krdp 6.7 creates its screencast session per RDP connection and drops
it when the connection closes, so krdpserver itself idles without one and stays up.

The bridge now counts established connections to Xvnc's port with ss, starts FreeRDP
when a viewer appears (the desktop shows about 3 s later; the VNC screen is black until
then) and stops it 45 s after the last one leaves (VNC_IDLE_SEC). Xvnc has no client
hook, so its log output, which it writes for every connection, wakes the bridge early;
otherwise it looks every 5 s while idle (0.1% of a core measured, against 0.9% for ss
once a second) and every second while FreeRDP runs. The layout check runs only while
FreeRDP runs.

While a viewer is connected the bridge sends "watch 15" to ft-screens (@ft_screens) at
once and every 5 s, so screens at a reduced frame rate (out of view, headset on a
stand) stream at full rate; it lapses by itself if the bridge dies, and an ft-screens
without the command just answers an error. The window search after starting FreeRDP
now ends when FreeRDP exits instead of polling for 30 s.

krdp on 127.0.0.1 with a fresh password, VNC on the tailnet address with VncAuth, and
remote-ctl.sh start/stop (pause and resume) are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 09:14:25 -06:00

180 lines
7.1 KiB
Bash
Executable File

#!/bin/bash
# Runs on the Frame host. Serves the Frametop desktop's primary screen (the one with the
# taskbar) over VNC for clients like RealVNC Viewer or macOS Screen Sharing. No VNC server
# here can capture KWin directly, so this bridges through krdp: Xvnc (a virtual X screen
# served over VNC) runs a FreeRDP client connected to krdpserver on 127.0.0.1. Both run in
# the dev container. VNC listens on the tailnet address only.
# Started by frametop-session.sh when REMOTE=1, after remote-desktop.sh.
#
# krdp streams the whole workspace (every screen). Its --monitor would stream one screen,
# but krdp 6.7 then maps the pointer as if that screen sat at 0,0, so clicks miss on a
# screen placed lower or further right. Instead the VNC screen is the primary's size, and
# the workspace-sized RDP window inside it is shifted so the primary fills it. The pointer
# maps 1:1. When the layout changes, the VNC screen resizes and the RDP client reconnects.
#
# The RDP client runs only while someone watches. While connected, krdp captures and
# H.264-encodes every redraw in software, about 60% of a core, with FreeRDP and Xvnc adding
# about 30% more, even with no VNC viewer. krdp starts its capture per RDP connection and
# stops it when the connection closes, so it idles without one. So FreeRDP starts when a
# VNC client connects (the screen is black for the few seconds that takes) and stops
# VNC_IDLE_SEC (45) seconds after the last one leaves. Xvnc has no hook for clients, so ss
# counts them: whenever Xvnc writes to its log (it logs each connection), every second
# while FreeRDP runs, and every 5 seconds otherwise. The log only wakes this script up;
# what it says doesn't matter.
set -eu
here=$(dirname "$(readlink -f "$0")")
vnc_port=${VNC_PORT:-5900}
rdp_port=${RDP_PORT:-3390}
display=:20
creds=$HOME/.config/frametop-remote
addr=$(ip -4 -o addr show tailscale0 2>/dev/null | awk '{print $4}' | cut -d/ -f1)
if [ -z "$addr" ]; then
echo "tailscale0 has no address, not starting VNC" >&2
exit 1
fi
# The VNC password is limited to 8 characters by the protocol. The traffic is
# still encrypted by the tailnet (WireGuard).
if [ ! -s "$creds/vnc-password" ]; then
(umask 077; head -c 12 /dev/urandom | base64 | tr -d '/+=' | cut -c1-8 > "$creds/vnc-password")
fi
# Wait for krdpserver (started by remote-desktop.sh).
for _ in $(seq 60); do
ss -ltn | grep -q "127.0.0.1:$rdp_port " && break
sleep 1
done
# "x y width height workspace_width workspace_height" of the primary screen, once Plasma is up.
view() { "$here/../layout/ft-layout" remote-view 2>/dev/null | grep -xE '[0-9]+( [0-9]+){5}'; }
v=
for _ in $(seq 90); do
v=$(view) && [ -n "$v" ] && break
v=
sleep 1
done
if [ -z "$v" ]; then
echo "couldn't read the desktop's screens, not starting VNC" >&2
exit 1
fi
read -r _ _ w h _ _ <<< "$v"
export XDG_RUNTIME_DIR=/run/user/$(id -u)
box() { "$HOME/.local/bin/distrobox" enter dev -- "$@"; }
stop_rdp() { pkill -f "[x]freerdp /v:127.0.0.1:$rdp_port " 2>/dev/null || true; }
trap 'stop_rdp; pkill -f "[X]vnc $display " 2>/dev/null || true' EXIT
box bash -c 'vncpasswd -f < "$1/vnc-password" > "$1/vnc-passwd.bin" && chmod 600 "$1/vnc-passwd.bin"' - "$creds"
exec {xlog}< <(box Xvnc "$display" -geometry "${w}x${h}" -depth 24 \
-interface "$addr" -rfbport "$vnc_port" \
-SecurityTypes VncAuth -PasswordFile "$creds/vnc-passwd.bin" \
-AlwaysShared -desktop "Steam Frame (Frametop)" 2>&1)
xvnc=$!
# Wait up to $1 seconds, less if Xvnc logs something; its lines go on to this log.
nap() {
local line rc=0
IFS= read -rt "$1" -u "$xlog" line || rc=$?
if [ $rc -eq 0 ]; then
printf '%s\n' "$line"
while IFS= read -rt 0.1 -u "$xlog" line; do printf '%s\n' "$line"; done
elif [ $rc -le 128 ]; then
sleep 1 # Xvnc's output closed: it's exiting
fi
return 0
}
nap 2
# A connected VNC client: an established TCP connection to Xvnc's port. Any connection
# counts, authenticated or not; it's on the tailnet only.
clients() { [ -n "$(ss -Htn state established "( sport = :$vnc_port )" 2>/dev/null)" ]; }
# ft-screens drops screens you aren't looking at to a low frame rate, and krdp would
# stream that. "watch SECONDS" asks it for full rate on every screen for that long: sent
# when a client connects and renewed every few seconds while one stays, so it lapses by
# itself if this script dies. An older ft-screens just answers that it doesn't know it.
watch() {
if command -v socat >/dev/null; then
printf 'watch 15' | socat -u - ABSTRACT-SENDTO:ft_screens 2>/dev/null
else
python3 -c 'import socket; socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM).sendto(b"watch 15", "\0ft_screens")' 2>/dev/null
fi || true
}
# Start FreeRDP inside the VNC screen, sized and shifted for $v.
# /cert:ignore is fine here: the connection never leaves this host.
start_rdp() {
read -r x y w h ww wh <<< "$v"
box env DISPLAY=$display bash -c '
size=$1 x=$2 y=$3 ww=$4 wh=$5 creds=$6 rdp_port=$7
if [ "$(xrandr | sed -n "s/.*current \([0-9]*\) x \([0-9]*\),.*/\1x\2/p")" != "$size" ]; then
xrandr --newmode "$size" 0 "${size%x*}" 0 0 0 "${size#*x}" 0 0 0 2>/dev/null || true
xrandr --addmode VNC-0 "$size" 2>/dev/null || true
xrandr --fb "$size" --output VNC-0 --mode "$size"
fi
xfreerdp /v:127.0.0.1:"$rdp_port" /u:"$(id -un)" /p:"$(cat "$creds/password")" \
/cert:ignore /size:"${ww}x${wh}" -decorations +clipboard >/dev/null 2>&1 &
rdp=$!
# FreeRDP takes no negative position, so move its window once it is up.
for _ in $(seq 60); do
kill -0 $rdp 2>/dev/null || break
win=$(xdotool search --class xfreerdp 2>/dev/null | tail -1)
[ -n "$win" ] && break
sleep 0.5
done
[ -n "$win" ] && xdotool windowmove "$win" "$((-x))" "$((-y))"
wait $rdp
' vnc-rdp "${w}x$h" "$x" "$y" "$ww" "$wh" "$creds" "$rdp_port" || true &
rdp=$!
}
idle_sec=${VNC_IDLE_SEC:-45}
rdp= # FreeRDP's job while it runs
seen=0 # when a client was last seen ($SECONDS)
watched=-99 # when "watch" was last sent
next_view=0 # next time to read the layout
while kill -0 $xvnc 2>/dev/null; do
if clients; then
if [ $((SECONDS - watched)) -ge 5 ]; then watch; watched=$SECONDS; fi
seen=$SECONDS
if [ -z "$rdp" ]; then
echo "VNC client connected, starting the RDP client"
now=$(view) && [ -n "$now" ] && v=$now
next_view=$((SECONDS + 5))
start_rdp
fi
elif [ "$seen" -ne 0 ]; then
watched=-99
if [ $((SECONDS - seen)) -ge "$idle_sec" ]; then
seen=0
if [ -n "$rdp" ]; then
echo "no VNC client for ${idle_sec}s, stopping the RDP client"
stop_rdp
wait "$rdp" 2>/dev/null || true
rdp=
fi
fi
fi
if [ -n "$rdp" ] && ! kill -0 "$rdp" 2>/dev/null; then
# It dropped, or the layout changed: reconnect next round if a client is still there.
wait "$rdp" 2>/dev/null || true
rdp=
nap 2
continue
fi
if [ -n "$rdp" ]; then
if [ "$SECONDS" -ge "$next_view" ]; then
next_view=$((SECONDS + 5))
now=$(view) || now=
if [ -n "$now" ] && [ "$now" != "$v" ]; then
echo "layout changed: $v -> $now"
v=$now
stop_rdp
fi
fi
fi
if [ -n "$rdp" ] || [ "$seen" -ne 0 ]; then nap 1; else nap 5; fi
done