From 64a6a1a078bf86c9b68a2bdea95a2420dffba4f3 Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 15 Oct 2025 00:17:29 +0300 Subject: [PATCH] fix: resolve intermittent mouse control loss and add permission logging This commit addresses three critical issues discovered during testing: Issue 1 - Intermittent mouse control loss requiring page refresh: When a session was promoted to primary, the HID queue handlers were fetching a fresh session copy from the session manager instead of using the original session pointer. This meant the queue handler had a stale Mode field (observer) while the manager had the updated Mode (primary). The permission check would fail, silently dropping all mouse input until the page was refreshed. Issue 2 - Missing permission failure diagnostics: When keyboard/mouse input was blocked due to insufficient permissions, there was no debug logging to help diagnose why input wasn't working. This made troubleshooting observer mode issues extremely difficult. Issue 3 - Session timeout despite active jiggler: The server-side jiggler moves the mouse every 30s after inactivity to prevent screen savers, but wasn't updating the session's LastActive timestamp. This caused sessions to timeout after 60s even with the jiggler active. Issue 4 - Session flapping after emergency promotion: When a session timed out and another was promoted, the newly promoted session had a stale LastActive timestamp (60+ seconds old), causing immediate re-timeout. This created an infinite loop where both sessions rapidly alternated between primary and observer every second. Issue 5 - Unnecessary WebSocket reconnections: The WebSocket fallback was unconditionally closing and reconnecting during emergency promotions, even when the connection was healthy. This caused spurious "Connection Issue Detected" overlays during normal promotions. Changes: - webrtc.go: Use original session pointer in handleQueues() (line 197) - hidrpc.go: Add debug logging when permission checks block input (lines 31-34, 61-64, 75-78) - jiggler.go: Update primary session LastActive after mouse movement (lines 146-152) - session_manager.go: Reset LastActive to time.Now() on promotion (line 1090) - devices.$id.tsx: Only reconnect if connection is unhealthy (lines 413-425) This ensures: 1. Queue handlers always have up-to-date session state 2. Permission failures are visible in logs for debugging 3. Jiggler prevents both screen savers AND session timeout 4. Newly promoted sessions get full timeout period (no immediate re-timeout) 5. Emergency promotions only reconnect when connection is actually stale 6. No spurious "Connection Issue" overlays during normal promotions --- jiggler.go | 15 +++++++++------ session_manager.go | 1 + ui/src/routes/devices.$id.tsx | 23 +++++++++++++++++------ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/jiggler.go b/jiggler.go index 3323d0bb..f7cf3835 100644 --- a/jiggler.go +++ b/jiggler.go @@ -129,19 +129,22 @@ func runJiggler() { } inactivitySeconds := config.JigglerConfig.InactivityLimitSeconds timeSinceLastInput := time.Since(gadget.GetLastUserInputTime()) - logger.Debug().Msgf("Time since last user input %v", timeSinceLastInput) if timeSinceLastInput > time.Duration(inactivitySeconds)*time.Second { - logger.Debug().Msg("Jiggling mouse...") - //TODO: change to rel mouse - // Use direct hardware calls for jiggler - bypass session permissions - err := gadget.AbsMouseReport(1, 1, 0) + err := gadget.RelMouseReport(1, 0, 0) if err != nil { logger.Warn().Msgf("Failed to jiggle mouse: %v", err) } - err = gadget.AbsMouseReport(0, 0, 0) + time.Sleep(50 * time.Millisecond) + err = gadget.RelMouseReport(-1, 0, 0) if err != nil { logger.Warn().Msgf("Failed to reset mouse position: %v", err) } + + if sessionManager != nil { + if primarySession := sessionManager.GetPrimarySession(); primarySession != nil { + sessionManager.UpdateLastActive(primarySession.ID) + } + } } } } diff --git a/session_manager.go b/session_manager.go index ed619ed0..80646413 100644 --- a/session_manager.go +++ b/session_manager.go @@ -1087,6 +1087,7 @@ func (sm *SessionManager) transferPrimaryRole(fromSessionID, toSessionID, transf // Promote target session toSession.Mode = SessionModePrimary toSession.hidRPCAvailable = false // Force re-handshake + toSession.LastActive = time.Now() // Reset activity timestamp to prevent immediate timeout sm.primarySessionID = toSessionID // ALWAYS set lastPrimaryID to the new primary to support WebRTC reconnections diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index 2ad16bbe..c5ba7149 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -400,18 +400,29 @@ export default function KvmIdRoute() { const { newMode, action } = parsedMessage.data; if (action === "reconnect_required" && newMode) { - console.log(`[Websocket] Mode changed to ${newMode}, reconnecting...`); - + // Update session state immediately if (currentSessionId) { setCurrentSession(currentSessionId, newMode); } + // Trigger RPC event handler handleRpcEvent("connectionModeChanged", parsedMessage.data); - setTimeout(() => { - peerConnection?.close(); - setupPeerConnection(); - }, 500); + // Only reconnect if the peer connection is actually stale + // If already connected, the mode change via RPC is sufficient + const isConnectionHealthy = + peerConnection?.connectionState === "connected" && + peerConnection?.iceConnectionState === "connected"; + + if (!isConnectionHealthy) { + console.log(`[Websocket] Mode changed to ${newMode}, connection unhealthy, reconnecting...`); + setTimeout(() => { + peerConnection?.close(); + setupPeerConnection(); + }, 500); + } else { + console.log(`[Websocket] Mode changed to ${newMode}, connection healthy, skipping reconnect`); + } } } },