Repository navigation
Description
Repository: Termix-SSH/Plugin-Remote-Desktop
Version: plugin 1.3.1 / Termix 26.11.0
Where: src/frontend/tunnel-watch.ts L5-6 and L58-69; src/frontend/GuacamoleDisplay.tsx L851-861
What happens
With an RDP session open and working, switch to another browser tab (or minimize the window) for a
few seconds and come back. If nothing on the remote desktop changes during the following 8 seconds,
the session is declared dead: the display goes blank, Termix logs reconnecting after lost and opens
a brand-new guacd connection. After three such events it stops reconnecting and shows a connection
error instead.
Nothing was actually lost. The WebSocket is still open the whole time — the session is killed by a
local liveness heuristic, not by a network failure.
Observed matrix (one Termix instance, one RDP host, Chrome 141 on Linux/Windows):
| Action after the tab becomes visible again | Result |
|---|---|
| Move the mouse / type within 8 s | session survives |
| Leave the desktop idle (cursor untouched, no window animation) | torn down and reconnected |
| Switch tab, come back, immediately click into the session | usually survives |
| Minimize the window, restore after ~10 s, wait | almost always torn down |
| Keep the tab visible the whole time and never touch it | session survives (the check only runs on visibilitychange) |
The reconnect is visible in the session log panel as a warning
remoteDesktop.reconnectingAfterLost, and in docker logs termix-guacd as an extra
Creating new client for protocol "rdp" for a user that never closed the session.
Relation to #30 / #35 (not a duplicate)
#30 asks for a way to turn off auto-reconnect, and #35 (merged into dev-1.4.0) adds an
Automatically reconnect RDP toggle. That is a useful escape hatch, but it does not address what
this report is about: the watchdog fires on sessions that were never lost. With the toggle left
at its default — enabled, because #35 preserves existing behaviour — everything below still happens
on every tab switch, and the user's only remedy is to disable the recovery feature altogether.
- Disable RDP auto-reconnect #30 / feat: allow disabling automatic RDP reconnection #35: "I don't want it to reconnect."
- this issue: "it reconnects because it wrongly believes the session died."
If the toggle ships, the reproduction below still applies with the default setting, which is what
almost every user runs.
Why
tunnel-watch.ts counts Guacamole instructions received and treats "no instruction within
ALIVE_CHECK_MS" as a dead tunnel:
// src/frontend/tunnel-watch.ts L5-6
/** How long a page back from hiding waits for anything from the server. It sends a nop every 5 s. */
export const ALIVE_CHECK_MS = 8000;
// src/frontend/tunnel-watch.ts L57-69
return {
checkAlive() {
if (stopped) return;
if (typeof tunnel.state === "number" && tunnel.state !== OPEN) {
lost("stale");
return;
}
const before = received;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
if (received === before) lost("stale"); // ← fires on a quiet desktop
}, checkMs);
},and GuacamoleDisplay.tsx starts that timer the moment the page becomes visible again:
// src/frontend/GuacamoleDisplay.tsx L851-861
const handleVisibilityChange = () => {
windowFocusedRef.current =
document.visibilityState === "visible" && document.hasFocus();
if (document.visibilityState !== "visible") {
hasKeyboardFocusRef.current = false;
} else if (wasConnectedRef.current) {
// The socket may have died while the tab was hidden or the screen locked.
tunnelWatchRef.current?.checkAlive();
}
refreshKeyboardHandlers();
};Three things make this a false positive on a normal session:
-
nopcannot be used to prove liveness. The comment on L5 assumes a periodicnopproduces
traffic that will be observed. It does not. From the Guacamole protocol reference,
"The 'nop' instruction does absolutely nothing, has no parameters, and is universally ignored
by both Guacamole clients and servers. Its main use is as a keep-alive signal …". Sending
nopkeeps NAT/firewall state alive but produces no inbound instruction, soreceivednever
increases because of it. In the shipped bundlePING_FREQUENCYis500(ms), not 5 s, which
only makes the point sharper: the client is chattering every half second andreceivedstill
stays frozen. -
A static RDP desktop legitimately produces no frames. guacd only sends
img/blob/sync
when something changes. An idle Windows desktop with no clock animation and no cursor movement
emits nothing for arbitrary lengths of time. 8 seconds of silence is the normal case, not the
failure case. -
Background throttling widens the gap. While the tab is hidden the browser throttles timers
(Chrome: down to once per minute for hidden tabs), so queued instructions may not be dispatched
until the page is visible again. The check then starts its 8 s window at exactly the moment the
backlog is being processed, so it is racing the event loop rather than measuring the socket.
The net effect: the watchdog cannot distinguish "quiet desktop" from "dead socket", and it fires in
the direction of destroying a healthy session.
Steps to reproduce
- Open an RDP host in Termix (either engine; the watchdog is frontend-only).
- Wait for the desktop to finish painting and stop interacting with it — no cursor movement, no
open animation/marquee, no clock with seconds. - Switch to another tab or minimise the window for ~10 s.
- Return to the Termix tab and do not touch anything.
- Within ~8 s the display clears and the session reconnects. The session log shows
reconnecting after lost. - Repeat twice more — after
MAX_AUTO_RECONNECTS = 3the session stops recovering by itself.
Counter-check that isolates the heuristic from the network: keep the tab visible and idle for the
same 10 s. Nothing happens, because checkAlive() only runs from visibilitychange.
What I expected
Coming back to a tab leaves a working session working. A watchdog should only tear down a session
when the transport is actually gone, and a quiet desktop is not evidence that it is.
Suggested fix
- Do not use "instructions received" as the liveness signal. The transport already knows:
tunnel.statebecomesCLOSEDon a real close, and theonstatechangewrapper at L51-55
already handles that case. Keeping a frozen screen behind a dead socket only matters when the
socket died without a close frame, which is what a round-trip probe is for. - If a probe is wanted, use one that is answered.
syncis the protocol round-trip: the
client sendssync <timestamp>and the server replies withsync <timestamp>. Waiting for a
syncecho would distinguish "quiet" from "dead" correctly, andnopexplicitly cannot. - At minimum, stop running
checkAlive()fromvisibilitychange. Re-arming the timer on every
tab switch guarantees that a single missed frame is enough to lose the session; the stale-socket
case the comment names (L857) is already covered byonstatechange. - Add a
console.debug/log line on thestaleverdict includingreceived,tunnel.stateand
the measured interval — right now the only user-visible trace is the reconnect itself.
Environment
- Termix 26.11.0, Plugin Remote Desktop 1.3.1, Docker on Linux (
ghcr.io/termix-ssh/termix:latest) termix-guacdguacamole/guacd:1.6.0as an external sidecar innetwork_mode: container:<termix-core>,GUACD_HOST=127.0.0.1- Target: Windows Server 2016 over RDP, 1280×720,
resize-method: display-update - Browsers: Chrome 141 and Edge 141 on Linux and Windows 11; Firefox reproduces identically
- Page served over plain HTTP (irrelevant here — the watchdog is scheme-independent)
Anything else
I read main rather than guessing: the line numbers above are from
src/frontend/tunnel-watch.ts (ALIVE_CHECK_MS = 8000, MAX_AUTO_RECONNECTS = 3) and
src/frontend/GuacamoleDisplay.tsx on main today. The shipped 1.3.1 bundle contains the same
logic, so this is not a stale-build artefact.
The intent of the watchdog is clear and the failure it guards against — a socket that dies while the
tab is hidden, leaving a frozen screen behind a dead transport — is real. The fix is only to use a
signal that can actually tell silence from death.
Happy to test a patch or open a PR.
Activity
termix-triage-bot commentedon Nov 7, 2026
Auto-triage
RDP sessions are incorrectly torn down and reconnected when the browser tab is hidden and then restored, if the desktop is idle. The maintainer has confirmed this is fixed in the dev branch.
A maintainer will take a look. If anything above looks wrong, just say so.
fix: keep an idle rdp session when the tab comes back (#39)
LukeGus commentedon Nov 7, 2026
Fixed on dev-1.4.0. A quiet desktop is no longer read as a dead connection when you come back to the tab. Only a tunnel that is really closed triggers a reconnect now.
termix-triage-bot commentedon Nov 9, 2026
This was fixed in v1.4.0. Update this plugin to the latest version to get it. Thanks for the report!