Merge branch perf-session into perf

This commit is contained in:
DeeJanuz committed 2026-10-03 09:22:07 -06:00
commit d6930a61f5
8 files changed
+195 -24

No files matched your search

+2 -1
View File
@@ -144,7 +144,8 @@ This is an early release, tested on one Steam Frame (SteamOS 0.3.0 build 2026092
- Dragging something from one panel to another (a screen and a floating window) works, but the dragged item's icon doesn't show while the pointer is between panels.
- Gaze mode is only as good as its calibration, and that depends on how the headset sits on your face. If the pointer lands off after you adjust the headset, run Quick check or Calibrate on the Gaze page of Frametop Input Settings.
- On SteamVR's Settings page, the 3D mouse shows a laser beam and a larger hit dot, like a controller. SteamVR doesn't tell other programs where that page is (unlike Steam's pages, such as Library), so the mouse used to miss most of it: clicks went through to a desktop screen behind, and the dot disappeared. As a workaround, on that page only, the laser starts near your eye and SteamVR finds the page itself. See docs/design.md.
- Remote desktop over VNC (Frametop Remote Access in the app menu, or `./desktops.sh remote on`) needs Tailscale on the Frame. It shows the primary screen only. The app turns it on and off, shows the address, and shows, copies, or changes the VNC password. The password is made at random on the Frame and kept in `~/.config/frametop-remote` (only you can read it); VNC limits it to 8 characters, and the tailnet encrypts the connection. Turning it on in a desktop that started with it off takes a desktop restart.
- Remote desktop over VNC (Frametop Remote Access in the app menu, or `./desktops.sh remote on`) needs Tailscale on the Frame. It shows the primary screen only. The app turns it on and off, shows the address, and shows, copies, or changes the VNC password. The password is made at random on the Frame and kept in `~/.config/frametop-remote` (only you can read it); VNC limits it to 8 characters, and the tailnet encrypts the connection. Turning it on in a desktop that started with it off takes a desktop restart. It costs almost nothing until a viewer connects; the picture then takes a few seconds to appear.
- The desktop has no blur behind panels and menus, and no window animations, so it leaves the headset's GPU to SteamVR. Turn them back on in the Frametop desktop's System Settings (Desktop Effects, and Animation speed under General Behavior); Frametop won't turn them off again.
- Turning the displays off on a stand only turns their backlight off. SteamVR has no way for other programs to put the headset in standby, so tracking and rendering keep running, and the headset draws nearly its full power.
## Reporting problems
+6
View File
@@ -177,6 +177,12 @@ The private runtime directory also moves the session's document portal to `$XDG_
A podman container's monitor process (conmon) stays in the cgroup of whatever started the container, and `distrobox enter` starts it on demand. When a Frametop service happened to start the `dev` container, stopping that service stopped the container and everything in it, including the desktop's compositor. `scripts/container-up.sh` starts the container in a systemd scope of its own before anything enters it. It then waits for distrobox-init to log `container_setup_done`, as `distrobox enter` does only for containers it starts itself. A new container's first start takes a minute or more (it installs distrobox's dependencies and sets up passwordless sudo), and an install that entered right away met a sudo password prompt with no terminal to answer it ([#9](https://github.com/DeeJanuz/frametop/issues/9)).
KWin renders with OpenGL through zink on Turnip, Vulkan on the same GPU vrcompositor needs to hit its frame time, and on the Frame that costs CPU too. The nested session started with KWin's defaults: blur and background contrast on (no `[Plugins]` group in its kwinrc) and animations at full length. Blur re-renders what's behind every translucent panel and menu each time it changes, and every animated frame is one more frame for KWin and ft-screens to draw and send. They're off by default in the Frametop desktop. The session script writes them before KWin starts, only where the desktop's own config has no value, once: System Settings deletes a setting put back to KDE's default rather than writing it, so without the marker in `frametoprc` a user who turned blur back on would lose it at the next start. The effect ids (`blur`, `contrast`) are the ones built into KWin 6.2.5 on SteamOS; KWin reads `<id>Enabled` from `[Plugins]`.
The nested session also runs the system's XDG autostart entries, being a KDE session. Discover's update notifier started `plasma-discover --mode update` in it (520 to 620 MB resident and about 9% of a core, plus `flatpak-system-helper` and AppStream downloads), and IBus started a daemon, the kimpanel panel and its GTK extension that nothing can use: KWin hands text input to the one input method it starts (`ft-textinput`), and the session drops `QT_IM_MODULE`, `GTK_IM_MODULE` and `XMODIFIERS`. The session hides both for this desktop only, with `Hidden=true` copies in its own autostart folder. The geoclue demo agent stays: it's what answers apps' location requests to Geoclue outside GNOME, and it costs nothing while idle. Orca's entry only starts in GNOME-family desktops.
Remote desktop is a chain (krdp, then FreeRDP inside Xvnc) because nothing on SteamOS serves KWin over VNC directly. Kept connected all the time, it cost about a core with nobody watching: krdpserver 55 to 78% (it encodes H.264 in software with openh264: VA-API finds no driver for the Frame's GPU in the container), FreeRDP 16 to 27%, Xvnc 6 to 11%, and the bridge's layout check every 5 seconds another 4%. krdp creates its screencast session per RDP connection and drops it when the connection closes (`SessionController::onNewConnection` in krdp 6.7), so an idle krdpserver costs nothing and can stay up; only the RDP connection has to go. The bridge connects FreeRDP when a VNC client appears and disconnects 45 seconds after the last one leaves. Xvnc has no hook for its clients, so the bridge counts established connections to its port with `ss`, woken early by Xvnc's log output; looking with `ss` once a second cost about 0.9% of a core in bash, against about 0.1% this way. `Xvnc -inetd` from a systemd socket would start a server per connection and lose sharing between viewers. The layout check (`ft-layout remote-view`, which scans `/proc` for plasmashell and runs `kscreen-doctor -j`) now runs only while FreeRDP runs, and then only after `kwinoutputconfig.json` or `frametop-layout.json` changes, with one check a minute in case a change touched neither.
Program names stay within 15 characters, because Linux truncates process names there and the scripts find programs with `pgrep -x` and `pkill -x`. That's why the prefix is `ft-`.
## Displays off on a stand
+12
View File
@@ -18,6 +18,16 @@ desktops.sh start | stop | restart | status | log [lines]
When the VR launcher starts the desktop, it inherits the Steam client's environment. The session script drops the client's runtime from it (`LD_LIBRARY_PATH`, the `STEAM_*` settings, and the Steam overlay's Vulkan layer), so apps in the desktop use the system's libraries, including its video codecs, just as they would after a normal login.
KWin's blur and background contrast effects and its animations are off in this desktop, because KWin draws on the headset's GPU, which SteamVR needs. The session script turns them off once, the first time it starts (it leaves a setting you already have alone, and marks it done in `~/.config/frametop/frametoprc`), so turning them back on sticks. In the Frametop desktop, System Settings → Window Management → Desktop Effects has Blur and Background Contrast, and General Behavior has Animation speed. Or from a terminal, then restart the desktop:
```
kwriteconfig6 --file ~/.config/frametop/kwinrc --group Plugins --key blurEnabled true
kwriteconfig6 --file ~/.config/frametop/kwinrc --group Plugins --key contrastEnabled true
kwriteconfig6 --file ~/.config/frametop/kdeglobals --group KDE --key AnimationDurationFactor 1
```
Two of the system's autostart programs don't start in this desktop: Discover's update notifier (`org.kde.discover.notifier`), which starts Discover to check for updates, and IBus (`ibus`), which can't reach the desktop's apps because KWin's input method is `input/ft-textinput`. The session script puts copies with `Hidden=true` in `~/.config/frametop/autostart` once (marked in `frametoprc`), and skips a name you already have a file for. Delete a copy to start that program again.
Settings are in two files, and Frametop Display Settings edits both. The screens (resolution, width in metres, scale, curve, which one has the taskbar) and their layout are in `~/.config/frametop-layout.json`. The backend, remote desktop, and pointer settings are in `~/.config/frametop.conf`; `session/frametop.conf.example` lists every key.
Restarting the desktop closes its windows. Before the unit stops, `session/keep-apps.sh` moves every program started in the desktop into a systemd scope of its own, so background work such as servers, tmux, and builds keeps running. An app that shuts down its own helper processes when its window closes will still lose them; run that kind of work outside the desktop, for example as a systemd user service.
@@ -262,6 +272,8 @@ It listens on port 5900 on the Frame's Tailscale address only, not the LAN, so i
No VNC server can capture KWin on SteamOS directly: `krfb` needs `xdg-desktop-portal-kde`, which SteamOS doesn't ship, and `wayvnc` only works with wlroots compositors. So `session/remote-desktop.sh` captures the desktop with KDE's `krdpserver --plasma` on `127.0.0.1:3390`, and `session/vnc-bridge.sh` runs TigerVNC's `Xvnc` on display `:20` with a FreeRDP client inside it and serves that. Both run in the `dev` container, and the extra hop adds a little latency. krdp streams every screen; the VNC screen is the primary's size, and the FreeRDP window is shifted so the primary fills it (`ft-layout remote-view` gives the offset). krdp's own `--monitor` would stream just one screen, but it maps the pointer as if that screen sat at 0,0, so clicks would miss. When the layout changes, the VNC screen resizes and FreeRDP reconnects within a few seconds.
FreeRDP runs only while a VNC viewer is connected, because while it's connected krdp captures and encodes every redraw. With no viewer, krdp has no RDP connection and so captures nothing, and Xvnc shows a black screen. When a viewer connects, the bridge starts FreeRDP, and the desktop appears about 3 seconds later; FreeRDP stops 45 seconds after the last viewer leaves (`VNC_IDLE_SEC` in the bridge's environment). The bridge looks for viewers with `ss` whenever Xvnc logs something, as it does for every connection, and every 5 seconds otherwise. While a viewer is connected, the bridge asks ft-screens to draw every screen at full rate (`watch 15` on `@ft_screens`, renewed every 5 seconds), so screens you aren't looking at in the headset, or a headset on a stand, don't stream at a low rate. It reads the primary screen's place again (`ft-layout remote-view`) only while FreeRDP runs, after `~/.config/frametop/kwinoutputconfig.json` or `~/.config/frametop-layout.json` changes, and once a minute.
With remote access on, the nested KWin runs with `KWIN_WAYLAND_NO_PERMISSION_CHECKS=1` and `KWIN_SCREENSHOT_NO_PERMISSION_CHECKS=1`, so any app in the Frametop desktop could capture its screens or inject input. The second one lets scripts take screenshots through KWin's `org.kde.KWin.ScreenShot2` D-Bus interface. This applies only to that desktop, not the stock one. Port 3389 is SteamOS's own `xrdp`, which starts a separate X11 session rather than showing the VR desktop.
## Limits
+10 -2
View File
@@ -306,14 +306,19 @@ int main() {
std::printf("displays on: %s\n", why.c_str());
};
// SteamVR is asked every 100 ms (events, the activity level, poses: IPC calls to
// vrserver). Input wakes the loop too, to wake the displays at once, but a moving mouse
// sends hundreds of events a second, so those wakes only drain the devices.
constexpr auto kTick = std::chrono::milliseconds(100);
auto lastVr = now - kTick;
std::vector<pollfd> fds;
while (!stopSignal) {
// Sleep until input or 100 ms: input wakes the displays at once, poses every tick.
fds.clear();
fds.push_back({ctl, POLLIN, 0});
for (auto &[path, d] : inputs)
if (d.fd >= 0) fds.push_back({d.fd, POLLIN, 0});
poll(fds.data(), fds.size(), 100);
const auto wait = std::chrono::ceil<std::chrono::milliseconds>(kTick - (Clock::now() - lastVr));
poll(fds.data(), fds.size(), std::clamp<int>(wait.count(), 0, kTick.count()));
now = Clock::now();
const bool grace = now < graceUntil;
std::string use; // why this tick counts as use
@@ -329,6 +334,8 @@ int main() {
lastConf = now;
}
if (now - lastVr >= kTick) {
lastVr = now;
vr::VREvent_t ev;
while (sys->PollNextEvent(&ev, sizeof ev)) {
if (ev.eventType == vr::VREvent_Quit) {
@@ -395,6 +402,7 @@ int main() {
use = "turned on elsewhere";
std::printf("displays on: turned on elsewhere\n");
}
}
if (!use.empty()) {
lastUse = now;
turnOn(use);
+15 -3
View File
@@ -83,6 +83,7 @@ class Window(Adw.ApplicationWindow):
self.status = {}
self.revealed = False
self.setting = False # the switch is being set from the status, not by the user
self.asking = False # a status check is running
self.toasts = Adw.ToastOverlay()
view = Adw.ToolbarView()
@@ -139,16 +140,27 @@ class Window(Adw.ApplicationWindow):
"/tmp/frametop-remote.log and /tmp/frametop-vnc.log.")
page.add(about)
# Each check runs remote-ctl.sh (bash, curl, and python3 for the tailnet name), so
# it runs every 5 seconds, when the window comes to the front, and after a change.
self.refresh()
GLib.timeout_add_seconds(2, self.refresh)
GLib.timeout_add_seconds(5, self.refresh)
self.connect("notify::is-active", lambda win, _param: win.is_active() and win.refresh())
# --- status ---
def refresh(self):
if not self.asking:
self.asking = True
proc = Gio.Subprocess.new([CTL, "status"], Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_SILENCE)
proc.communicate_utf8_async(None, None, self.on_status)
return True
def refresh_soon(self):
"""Again in a moment: remote access takes a second or two to start or stop."""
self.refresh()
GLib.timeout_add_seconds(2, lambda: self.refresh() and False)
def on_status(self, proc, result):
self.asking = False
try:
_, out, _ = proc.communicate_utf8_finish(result)
except GLib.Error:
@@ -197,7 +209,7 @@ class Window(Adw.ApplicationWindow):
self.toast("Remote access turns on at the next desktop restart")
else:
self.toast("Remote access on")
self.refresh()
self.refresh_soon()
def on_reveal(self, button):
self.revealed = button.get_active()
@@ -221,7 +233,7 @@ class Window(Adw.ApplicationWindow):
if self.status.get("running") == "1":
subprocess.run([CTL, "restart"], stdin=subprocess.DEVNULL, capture_output=True)
self.toast("New password set" + (": remote access restarted" if self.status.get("running") == "1" else ""))
self.refresh()
self.refresh_soon()
def copy(self, text, done):
if text:
+14 -6
View File
@@ -46,8 +46,9 @@ PACKAGES = {
"deckard-steamvr-rel": "the 3D mouse on the dashboard, on SteamVR Settings, and on a SteamVR "
"window's grab bar; picking up a controller; mapped controller buttons; gaze",
"kwin": "clicks near the far edge of a screen whose scale isn't 1; every screen comes back "
"after a desktop restart; floating a window",
"plasma-workspace": "the taskbar and panels after a desktop restart",
"after a desktop restart; floating a window; no blur behind the taskbar's menus",
"plasma-workspace": "the taskbar and panels after a desktop restart; no DiscoverNotifier or "
"ibus-daemon inside the desktop",
"gamescope": "the headset's volume buttons with nothing focused; typing goes where you last clicked",
"bluez": "a Bluetooth mouse reconnecting after it sleeps",
}
@@ -176,14 +177,21 @@ def check_host():
try:
with open("/usr/bin/kwin_wayland", "rb") as f:
# Qt keeps the option name as a UTF-16 string literal.
output_count = "output-count".encode("utf-16-le") in f.read()
kwin = f.read()
except OSError:
output_count = False
if output_count:
kwin = b""
# Qt keeps the option name as a UTF-16 string literal.
if "output-count".encode("utf-16-le") in kwin:
report("ok", "KWin", "has --output-count (one output per screen)")
else:
report("FAIL", "KWin", "no --output-count option: the desktop gets one screen at most")
# The session turns these built-in effects off by id (blurEnabled, contrastEnabled in kwinrc).
gone = [e for e in ("blur", "contrast") if b"KWin::%s_factory" % e.encode() not in kwin]
if not gone:
report("ok", "KWin effects", "blur and contrast, which the desktop turns off, keep their ids")
else:
report("warn", "KWin effects", f"no built-in {', '.join(gone)} effect: renamed? The desktop's "
"kwinrc may no longer turn it off (session/frametop-session.sh)")
if systemctl("cat", "steamvr.service"):
report("ok", "steamvr.service", "Frametop's services start and stop with it")
+34
View File
@@ -234,6 +234,40 @@ elif [ "$(kreadconfig6 --file "$kwinrc" --group org.kde.kdecoration2 --key theme
kwriteconfig6 --file "$kwinrc" --group org.kde.kdecoration2 --key theme --delete
fi
# KWin draws through zink on the headset's GPU, which vrcompositor needs, so the blur and
# background contrast behind panels and menus and the window animations cost frames for
# little. They're off by default in this desktop. Each is written once, before KWin first
# reads it, and only if this desktop's config doesn't have it yet; the marker keeps it
# from coming back, since System Settings deletes a setting put back to KDE's default.
# docs/reference.md says how to turn them on again.
default() { # file group key value
[ -n "$(kreadconfig6 --file "$1" --group "$2" --key "$3")" ] || kwriteconfig6 --file "$1" --group "$2" --key "$3" "$4"
}
frametoprc=$XDG_CONFIG_HOME/frametoprc
if [ "$(kreadconfig6 --file "$frametoprc" --group Defaults --key effects)" != 1 ]; then
default "$kwinrc" Plugins blurEnabled false
default "$kwinrc" Plugins contrastEnabled false
default "$XDG_CONFIG_HOME/kdeglobals" KDE AnimationDurationFactor 0
kwriteconfig6 --file "$frametoprc" --group Defaults --key effects 1
fi
# System autostart entries this desktop doesn't need, hidden for it alone by a copy with
# Hidden=true in its own autostart folder, once and unless there's a file of that name
# already. Discover's update notifier starts Discover itself to check for updates (about
# 600 MB and a share of a core, with Flatpak's helper and AppStream behind it); updates
# come with SteamOS and from Discover in the stock desktop. IBus can't reach the
# desktop's apps: KWin's input method is ft-textinput, and the session drops the
# variables that point apps at IBus or XIM. Deleting the copy brings an entry back.
if [ "$(kreadconfig6 --file "$frametoprc" --group Defaults --key autostart)" != 1 ]; then
for entry in org.kde.discover.notifier ibus; do
src=/etc/xdg/autostart/$entry.desktop dst=$XDG_CONFIG_HOME/autostart/$entry.desktop
[ -r "$src" ] && [ ! -e "$dst" ] || continue
mkdir -p "$(dirname "$dst")"
sed '/^\[Desktop Entry\]$/a Hidden=true' "$src" > "$dst"
done
kwriteconfig6 --file "$frametoprc" --group Defaults --key autostart 1
fi
# Profiles reopen apps (docs/profiles.md), so Plasma's own session restore stays off here;
# with both, apps would open twice.
kwriteconfig6 --file "$XDG_CONFIG_HOME/ksmserverrc" --group General --key loginMode emptySession
+102 -12
View File
@@ -11,6 +11,16 @@
# 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")")
@@ -57,16 +67,51 @@ 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"
box Xvnc "$display" -geometry "${w}x${h}" -depth 24 \
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)" &
-AlwaysShared -desktop "Steam Frame (Frametop)" 2>&1)
xvnc=$!
sleep 2
# Keep an RDP connection open inside the VNC screen. Reconnect if it drops or the layout changes.
# 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.
while kill -0 $xvnc 2>/dev/null; do
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
@@ -80,6 +125,7 @@ while kill -0 $xvnc 2>/dev/null; do
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
@@ -88,14 +134,58 @@ while kill -0 $xvnc 2>/dev/null; do
wait $rdp
' vnc-rdp "${w}x$h" "$x" "$y" "$ww" "$wh" "$creds" "$rdp_port" || true &
rdp=$!
while kill -0 $rdp 2>/dev/null; do
sleep 5
now=$(view) || continue
[ -n "$now" ] && [ "$now" != "$v" ] || continue
}
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
done
wait $rdp 2>/dev/null || true
sleep 2
fi
fi
fi
if [ -n "$rdp" ] || [ "$seen" -ne 0 ]; then nap 1; else nap 5; fi
done