mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 05:00:08 +02:00
While FreeRDP ran, vnc-bridge.sh called ft-layout remote-view every 5 s, which scans all of /proc for plasmashell and runs kscreen-doctor -j: about 4.4% of a core for a layout that rarely changes. It now stats the two files the answer depends on, the nested KWin's ~/.config/frametop/kwinoutputconfig.json (positions, scales, primary) and ~/.config/frametop-layout.json (screen sizes), once a second while FreeRDP runs. After either changes it reads the layout every 2 s for 10 s, since KWin's outputs follow the file a few seconds later; otherwise once a minute, in case a change touched neither. With no VNC viewer connected nothing runs (previous commit). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
192 lines
7.9 KiB
Bash
Executable File
192 lines
7.9 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
|
|
}
|
|
|
|
# What remote-view depends on: KWin's saved outputs (positions, scales, primary) and
|
|
# Frametop's layout (screen sizes). It's read again only after one of these changes, and
|
|
# once a minute in case a change didn't touch them, and only while FreeRDP runs.
|
|
layout_files=("$HOME/.config/frametop/kwinoutputconfig.json" "$HOME/.config/frametop-layout.json")
|
|
stamp() { stat -c %y "${layout_files[@]}" 2>/dev/null || 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
|
|
stamp_v= # stamp() when $v was read
|
|
recheck=0 # read the layout every 2 seconds until then
|
|
next_view=0 # next time to read it
|
|
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
|
|
stamp_v=$(stamp) recheck=0 next_view=$((SECONDS + 60))
|
|
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
|
|
# A layout change shows up in KWin's outputs a few seconds after the files change.
|
|
s=$(stamp)
|
|
[ "$s" = "$stamp_v" ] || { stamp_v=$s; recheck=$((SECONDS + 10)) next_view=$SECONDS; }
|
|
if [ "$SECONDS" -ge "$next_view" ]; then
|
|
next_view=$((SECONDS + 60))
|
|
[ "$SECONDS" -ge "$recheck" ] || next_view=$((SECONDS + 2))
|
|
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
|