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
This commit is contained in:
Alex P 2025-10-15 00:17:29 +03:00
parent 827decf803
commit 64a6a1a078
3 changed files with 27 additions and 12 deletions

View File

@ -129,19 +129,22 @@ func runJiggler() {
} }
inactivitySeconds := config.JigglerConfig.InactivityLimitSeconds inactivitySeconds := config.JigglerConfig.InactivityLimitSeconds
timeSinceLastInput := time.Since(gadget.GetLastUserInputTime()) timeSinceLastInput := time.Since(gadget.GetLastUserInputTime())
logger.Debug().Msgf("Time since last user input %v", timeSinceLastInput)
if timeSinceLastInput > time.Duration(inactivitySeconds)*time.Second { if timeSinceLastInput > time.Duration(inactivitySeconds)*time.Second {
logger.Debug().Msg("Jiggling mouse...") err := gadget.RelMouseReport(1, 0, 0)
//TODO: change to rel mouse
// Use direct hardware calls for jiggler - bypass session permissions
err := gadget.AbsMouseReport(1, 1, 0)
if err != nil { if err != nil {
logger.Warn().Msgf("Failed to jiggle mouse: %v", err) 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 { if err != nil {
logger.Warn().Msgf("Failed to reset mouse position: %v", err) logger.Warn().Msgf("Failed to reset mouse position: %v", err)
} }
if sessionManager != nil {
if primarySession := sessionManager.GetPrimarySession(); primarySession != nil {
sessionManager.UpdateLastActive(primarySession.ID)
}
}
} }
} }
} }

View File

@ -1087,6 +1087,7 @@ func (sm *SessionManager) transferPrimaryRole(fromSessionID, toSessionID, transf
// Promote target session // Promote target session
toSession.Mode = SessionModePrimary toSession.Mode = SessionModePrimary
toSession.hidRPCAvailable = false // Force re-handshake toSession.hidRPCAvailable = false // Force re-handshake
toSession.LastActive = time.Now() // Reset activity timestamp to prevent immediate timeout
sm.primarySessionID = toSessionID sm.primarySessionID = toSessionID
// ALWAYS set lastPrimaryID to the new primary to support WebRTC reconnections // ALWAYS set lastPrimaryID to the new primary to support WebRTC reconnections

View File

@ -400,18 +400,29 @@ export default function KvmIdRoute() {
const { newMode, action } = parsedMessage.data; const { newMode, action } = parsedMessage.data;
if (action === "reconnect_required" && newMode) { if (action === "reconnect_required" && newMode) {
console.log(`[Websocket] Mode changed to ${newMode}, reconnecting...`); // Update session state immediately
if (currentSessionId) { if (currentSessionId) {
setCurrentSession(currentSessionId, newMode); setCurrentSession(currentSessionId, newMode);
} }
// Trigger RPC event handler
handleRpcEvent("connectionModeChanged", parsedMessage.data); handleRpcEvent("connectionModeChanged", parsedMessage.data);
setTimeout(() => { // Only reconnect if the peer connection is actually stale
peerConnection?.close(); // If already connected, the mode change via RPC is sufficient
setupPeerConnection(); const isConnectionHealthy =
}, 500); 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`);
}
} }
} }
}, },