mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 06:00:09 +02:00
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>
180 lines
7.1 KiB
Bash
Executable File
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
|