Skip to content

Conversation

@pennycoders
Copy link
Contributor

@pennycoders pennycoders commented Oct 8, 2025

Multi-Session Support with Role-Based Access Control

Implements concurrent WebRTC session management with granular permission control, enabling multiple users to connect simultaneously with different access levels.

Overview

JetKVM now supports up to 10 simultaneous connections per device with automatic session management and conflict resolution. The multi-session system uses role-based permissions to control access and ensures system availability through intelligent session promotion algorithms.

Key Features

  • Multiple concurrent connections - Up to 10 simultaneous sessions per device
  • Role-based permissions - Four distinct session modes (Primary, Observer, Queued, Pending)
  • Automatic session promotion - Seamless handoff between primary sessions
  • Grace period reconnection - 10-second window to recover from accidental disconnects
  • Session approval workflow - Optional gating for new connections
  • Transfer protection - 60-second blacklist prevents unwanted session takeovers
  • Emergency promotion - Trust-based algorithm prevents deadlock situations
  • Activity timeout - Configurable inactivity timeout for primary sessions (default: 5 minutes)

Session State Flow

flowchart TD
    A[New Connection] --> B{Primary<br/>Exists?}
    B -->|No| C[Assign PRIMARY]
    B -->|Yes| D{Approval<br/>Required?}
    D -->|No| E[Assign OBSERVER]
    D -->|Yes| F[Assign PENDING]

    E --> G{Want<br/>Control?}
    G -->|Yes| H[Request Control]
    H --> I[Become QUEUED]
    I --> J{Primary<br/>Approves?}
    J -->|Yes| K[Transfer to PRIMARY]
    J -->|No| E

    F --> L{Approved by<br/>Primary?}
    L -->|Yes| E
    L -->|No| M[Disconnect]

    C --> N[Has Full Control]
    K --> N

    style C fill:#90EE90
    style E fill:#87CEEB
    style I fill:#FFD700
    style F fill:#FFB6C6
    style N fill:#98FB98
Loading

Grace Period & Reconnection

Protects against accidental disconnects (network issues, browser refresh, etc.):

flowchart TD
    A[Connection Lost] --> B[Start Grace Period<br/>10 seconds]
    B --> C{Was Primary?}
    C -->|Yes| D[Reserve Primary Slot]
    C -->|No| E[Track in Grace Map]
    D --> F{Reconnect Within<br/>Grace Period?}
    E --> F
    F -->|Yes| G[Restore Session<br/>Same Mode]
    F -->|No - Timeout| H{Was Primary?}
    H -->|Yes| I[Promote Next Session]
    H -->|No| J[Remove from Grace Map]
    G --> K[Session Active]
    I --> K

    style D fill:#FFD700
    style G fill:#90EE90
    style I fill:#87CEEB
Loading

Grace Period Details:

  • Configurable duration (1-300 seconds, default 10 seconds)
  • Maximum 10 concurrent grace periods (DoS protection)
  • Primary slot reserved during grace period
  • Automatic cleanup on expiration

Emergency Promotion Algorithm

Ensures system always has a primary session when sessions exist:

flowchart TD
    A[Emergency Promotion Triggered] --> B{Approval<br/>Required?}

    B -->|No| C[Simple First-Eligible]
    C --> D{Queued Sessions<br/>Exist?}
    D -->|Yes| E[Promote First<br/>Queued Not Blacklisted]
    D -->|No| F{Observer Sessions<br/>Exist?}
    F -->|Yes| G[Promote First<br/>Observer Not Blacklisted]
    F -->|No| H{Pending Sessions<br/>Exist?}
    H -->|Yes| I[Promote First<br/>Pending Not Blacklisted]
    H -->|No| J[No Sessions Available]

    B -->|Yes| K[Trust-Based Scoring]
    K --> L[Calculate Trust Scores:<br/>Age + Previous Primary +<br/>Mode + Nickname]
    L --> M{Observers/Queued<br/>Available?}
    M -->|Yes| N[Score Observers/Queued]
    M -->|No| O[Score Pending]
    N --> P[Select Highest Score]
    O --> P
    P --> Q[Promote Winner]

    E --> R[New Primary Active]
    G --> R
    I --> R
    Q --> R

    style E fill:#90EE90
    style G fill:#87CEEB
    style I fill:#FFB6C6
    style L fill:#FFD700
    style P fill:#DDA0DD
Loading

Trust Scoring Factors:

  • Session age (0-100 points, capped to prevent overflow)
  • Previous primary status (+50 points)
  • Current mode: Observer (+20), Queued (+10), Pending (0)
  • Nickname presence (+15 if required, -30 if missing when required)

Emergency Promotion Triggers:

  1. No primary exists (validation check every 10 seconds)
  2. Primary session timeout (default: 5 minutes of inactivity)
  3. Grace period expiration without reconnection

Rate Limiting (when approval required AND primary exists):

  • Sliding window: Max 3 promotions per 60 seconds
  • Cooldown: 10 seconds between emergency promotions
  • Consecutive: Max 3 consecutive emergency promotions
  • All limits bypassed when no primary exists (prevents deadlock)

Session Modes & Permissions

Primary (Full Control)

  • View video feed
  • Send keyboard/mouse input
  • Control power (ATX/DC)
  • Mount/unmount virtual media
  • Manage all settings
  • Approve/deny sessions
  • Transfer control to others
  • Access terminal/serial console
  • Timeout: 5 minutes inactivity (configurable)

Observer (View-Only)

  • View video feed
  • See mounted media (read-only)
  • Request primary control
  • View session list
  • Timeout: 30-60 seconds after RPC channel closes (configurable)

Queued (Waiting for Control)

  • All Observer permissions
  • Visible in primary's approval queue
  • Can be approved/denied by primary

Pending (Awaiting Access)

  • No video access
  • No input capabilities
  • Must be approved by primary to become Observer
  • Auto-removed after 1 minute if not approved (DoS protection)

Technical Implementation

Architecture

  • Permission System: Centralized RBAC in internal/session package (28 unique permissions)
  • Session Management: Concurrent-safe SessionManager with atomic operations
  • Emergency Promotion: Multi-layer rate limiting with trust-based selection
  • Grace Period: Time-bounded reconnection map with automatic cleanup
  • Transfer Blacklist: 60-second cooldown after manual transfers

Security & DoS Protection

  • Broadcast throttling (100ms global, 50ms per-session)
  • Defense-in-depth permission validation at RPC and method levels
  • Pending session timeout (1 minute)
  • Grace period limits (max 10 concurrent)
  • Emergency promotion rate limiting (3 layers)
  • Session count limits (configurable, default 10)
  • Rejection attempt tracking (3 strikes, 60-second cooldown)

Performance Optimizations

  • Pre-allocated event maps for hot-path operations
  • Lock-free session iteration with snapshot pattern
  • Efficient broadcast with throttling
  • Atomic nickname updates
  • Integer overflow protection in trust scoring

Session Settings

All settings configurable via UI or API:

Setting Default Range Description
Require Approval false boolean New sessions need primary approval
Require Nickname false boolean Sessions must set nickname
Reconnect Grace 10s 1-300s Grace period for reconnection
Primary Timeout 300s 0-3600s Inactivity timeout (0=disabled)
Observer Timeout 30s 0-3600s Cleanup timeout for closed observers
Private Keystrokes false boolean Hide keyboard input from observers
Max Sessions 10 1-50 Maximum concurrent sessions
Max Rejection Attempts 3 1-10 Strikes before 60s cooldown

Session Identification

  • Automatic nickname generation from User-Agent (e.g., "Chrome on macOS")
  • Manual nickname customization
  • Validation: 2-32 chars, alphanumeric + spaces + -_.@
  • Uniqueness enforcement

Activity Tracking

Primary inactivity timer resets on:

  • Keyboard input (HID RPC keyboard reports)
  • Mouse input (HID RPC mouse movement, clicks, wheel)
  • Any valid JSON-RPC method call

Explicitly excluded:

  • Jiggler automated mouse movement (intentional - maintains keyboard auto-release only)
  • WebSocket signaling messages
  • WebRTC connection keep-alives

Implements concurrent WebRTC session management with granular permission control, enabling multiple users to connect simultaneously with different access levels.

Features:
- Session modes: Primary (full control), Observer (view-only), Queued, Pending
- Role-based permissions (31 permissions across video, input, settings, system)
- Session approval workflow with configurable access control
- Primary control transfer, request, and approval mechanisms
- Grace period reconnection (prevents interruption on network issues)
- Automatic session timeout and cleanup
- Nickname system with browser-based auto-generation
- Trust-based emergency promotion (deadlock prevention)
- Session blacklisting (prevents transfer abuse)

Technical Implementation:
- Centralized permission system (internal/session package)
- Broadcast throttling (100ms global, 50ms per-session) for DoS protection
- Defense-in-depth permission validation
- Pre-allocated event maps for hot-path performance
- Lock-free session iteration with snapshot pattern
- Comprehensive session management UI with real-time updates

New Files:
- session_manager.go (1628 lines) - Core session lifecycle
- internal/session/permissions.go (306 lines) - Permission rules
- session_permissions.go (77 lines) - Package integration
- datachannel_helpers.go (11 lines) - Permission denied handler
- errors.go (10 lines) - Error definitions
- 14 new UI components (session management, approval dialogs, overlays)

50 files changed, 5836 insertions(+), 442 deletions(-)
@pennycoders pennycoders changed the title feat: multi-session support with role-based permissions Dynamic RBAC-ready multi-session support with role-based permissions Oct 8, 2025
@pennycoders pennycoders marked this pull request as draft October 8, 2025 15:59
@pennycoders pennycoders changed the title Dynamic RBAC-ready multi-session support with role-based permissions RBAC-ready multi-session support with role-based permissions Oct 8, 2025
Address all linting warnings and errors in both backend and frontend code:

**Go (golangci-lint):**
- Add error checking for ignored return values (errcheck)
- Remove unused RPC functions (unused)
- Fix import formatting (goimports)

**TypeScript/React (eslint):**
- Replace all 'any' and 'Function' types with proper type definitions
- Add RpcSendFunction type for consistent JSON-RPC callback signatures
- Fix React Hook exhaustive-deps warnings by adding missing dependencies
- Wrap functions in useCallback where needed to stabilize dependencies
- Remove unused variables and imports
- Remove empty code blocks
- Suppress exhaustive-deps warnings where intentional (with comments)

All linting now passes with 0 errors and 0 warnings.
@SilkePilon
Copy link
Contributor

Nice job with this one, will try it out myself. Any things i need to know about to set it up @pennycoders ?

CRITICAL SECURITY FIX: Pending sessions (awaiting approval) were granted
video.view permission, allowing denied sessions to see video when they
reconnected.

**Vulnerability:**
1. Session requests access and enters pending mode
2. Primary session denies the request
3. Denied session clicks "Try Again" and reconnects
4. New session enters pending mode but has video.view permission
5. User can see video stream despite being denied

**Fix:**
Remove PermissionVideoView from SessionModePending. Pending sessions now
have NO permissions until explicitly approved by the primary session.

This ensures:
- Denied sessions cannot access video on reconnection
- Only approved sessions (observer/queued/primary) can view video
- CanReceiveVideo() properly blocks video frames for pending sessions
… limits

Backend improvements:
- Keep denied sessions alive in pending mode instead of removing them
- Add requestSessionApproval RPC method for re-requesting access
- Fix security issue: preserve pending mode on reconnection for denied sessions
- Add MaxRejectionAttempts field to SessionSettings (default: 3, configurable 1-10)

Frontend improvements:
- Change "Try Again" button to "Request Access Again" that re-requests approval
- Add rejection counter with configurable maximum attempts
- Hide modal after max rejections; session stays pending in SessionPopover
- Add "Dismiss" button for primary to hide approval requests without deciding
- Add MaxRejectionAttempts control in multi-session settings page
- Reset rejection count when session is approved

This improves the user experience by allowing denied users to retry without
page reloads, while preventing spam with configurable rejection limits.
Sessions in pending mode do not have PermissionVideoView and should not
attempt to call getLocalVersion RPC method. Add permission check before
calling getLocalVersion to prevent unnecessary permission denied errors.
… logout promotion

Observer-to-primary promotion protections:
- Block auto-promotion during active primary grace periods
- Prevent creating multiple primary sessions simultaneously
- Validate transfer source is actual current primary
- Check for duplicate primaries before promotion

Immediate promotion on logout:
- Trigger validateSinglePrimary() immediately when primary disconnects
- Smart grace period bypass: allow promotion within 2 seconds of disconnect
- Provides instant promotion on logout while protecting against network blips

Enhanced validation and logging:
- Log session additions/removals with counts
- Display session IDs in validation logs for debugging
- Track grace period timing for smart bypass decisions
- Remove broken bypass logic that caused immediate observer promotion on refresh
- Add session map debugging logs to validateSinglePrimary
- Ensure grace period properly blocks auto-promotion until expiration
@pennycoders pennycoders force-pushed the feat/multisession-support branch from f9eda52 to 541d2bd Compare October 8, 2025 20:58
Add comprehensive logging to identify why sessions fail to be added to
the session manager:
- Log entry/exit points in AddSession
- Track reconnection path execution
- Log max sessions limit checks
- Trace AddSession call and return in handleSessionRequest

This will help diagnose why sessions get stuck at ICE checking state
without being properly registered in the session manager.
The previous limit of 20 RPC/second per session was too aggressive for
multi-session scenarios. During normal operation with multiple sessions,
legitimate RPC calls would frequently hit the rate limit, especially
during page refreshes or reconnections when sessions make bursts of calls
like getSessions, getPermissions, getLocalVersion, and getVideoState.

Increased the limit to 100 RPC/second per session, which still provides
DoS protection while accommodating legitimate multi-session usage patterns.
1. Terminal access permission check:
   - Add Permission.TERMINAL_ACCESS check to Web Terminal button
   - Prevents observer sessions from accessing terminal

2. Immediate websocket cleanup:
   - Close peer connection immediately when websocket errors
   - Previously waited 24+ seconds for ICE to transition from disconnected to failed
   - Now triggers session cleanup immediately on tab close

3. Immediate grace period validation:
   - Trigger validateSinglePrimary() immediately when grace period expires
   - Previously waited up to 10 seconds for next periodic validation
   - Eliminates unnecessary delay in observer promotion

Timeline improvement:
Before: Tab close → 6s (ICE disconnect) → 24s (ICE fail) → RemoveSession → 10s grace → up to 10s validation = ~50s total
After: Tab close → immediate peerConnection.Close() → immediate RemoveSession → 10s grace → immediate validation = ~11s total
…eriod promotion

When a primary session disconnects accidentally (not intentional logout), the
60-second transfer blacklist from previous role transfers was blocking observer
sessions from being promoted after the grace period expires (~10s).

The blacklist is intended to prevent immediate re-promotion during manual
transfers (user-initiated), but should not interfere with emergency promotion
after accidental disconnects (system-initiated).

Changes:
- Clear all transfer blacklist entries when primary enters grace period
- Add logging to track blacklist clearing for debugging
- Preserve blacklist during intentional logout to maintain manual transfer protection

This ensures observers are promoted after grace period (~10s) instead of
waiting for blacklist expiration (~40-60s).
When a user explicitly logs out via the logout button, the session should
be removed immediately without grace period, allowing observers to be
promoted right away instead of waiting for the grace period to expire.

Changes:
- Close WebRTC connection immediately on logout
- Clear grace period marker for intentional logout detection
- Add logging to track logout vs disconnect differentiation

This complements the accidental disconnect handling which uses grace period.
@pennycoders
Copy link
Contributor Author

pennycoders commented Oct 9, 2025

Nice job with this one, will try it out myself. Any things i need to know about to set it up @pennycoders ?

Nope, everything should just be built and deployed how it normally is. Just the fact that I did not get a chance to test it in "cloud mode", since I don't have a cloud environment that I can test with. Will get around to setting one up, but not sure when. When the password auth is disabled it is a little flaky, but from what I was able to test it works pretty well. I actually think it would be great if you could put it through its paces

The session manager had backwards logic that prevented sessions from
restoring their primary status when reconnecting within the grace period.
This caused browser refreshes to demote primary sessions to observers.

Changes:
- Fix conditional in AddSession to allow primary restoration within grace
- Remove excessive debug logging throughout session manager
- Clean up unused decrActiveSessions function
- Remove unnecessary leading newline in NewSessionManager
- Update lastPrimaryID handling to support WebRTC reconnections
- Preserve grace periods during transfers to allow browser refreshes

The fix ensures that when a primary session refreshes their browser:
1. RemoveSession adds a grace period entry
2. New connection checks wasWithinGracePeriod and wasPreviouslyPrimary
3. Session correctly reclaims primary status

Blacklist system prevents demoted sessions from immediate re-promotion
while grace periods allow legitimate reconnections.
…management

This commit addresses multiple CRITICAL and HIGH severity security issues
identified during the multi-session implementation review.

CRITICAL Fixes:
- Fix race condition in session approval handlers (jsonrpc.go)
  Previously approveNewSession and denyNewSession directly mutated
  session.Mode without holding the SessionManager.mu lock, potentially
  causing data corruption during concurrent access.

- Add validation to ApprovePrimaryRequest (session_manager.go:795-810)
  Now verifies that requester session exists and is in Queued mode
  before approving transfer, preventing invalid state transitions.

- Close dual-primary window during reconnection (session_manager.go:208)
  Added explicit primaryExists check to prevent brief window where two
  sessions could both be primary during reconnection.

HIGH Priority Fixes:
- Add nickname uniqueness validation (session_manager.go:152-159)
  Prevents multiple sessions from having the same nickname, both in
  AddSession and updateSessionNickname RPC handler.

Code Quality:
- Remove debug scaffolding from cloud.go (lines 515-520, 530)
  Cleaned up temporary debug logs that are no longer needed.

Thread Safety:
- Add centralized ApproveSession() method (session_manager.go:870-890)
- Add centralized DenySession() method (session_manager.go:894-912)
  Both methods properly acquire locks and validate session state.

- Update RPC handlers to use thread-safe methods
  approveNewSession and denyNewSession now call sessionManager methods
  instead of direct session mutation.

All changes have been verified with linters (golangci-lint: 0 issues).
…rate limit

Changes:
- Add permission checks before making getVideoState, getKeyboardLedState,
  and getKeyDownState RPC calls to prevent rejected requests for sessions
  without VIDEO_VIEW permission
- Fix infinite loop issue by excluding hasPermission from useEffect
  dependency arrays (functions recreated on render cause infinite loops)
- Increase RPC rate limit from 100 to 500 per second to support 10+
  concurrent sessions with broadcasts and state updates

This eliminates console spam from permission denied errors and log spam
from continuous RPC calls, while improving multi-session performance.
The getLocalVersion useEffect had getLocalVersion and hasPermission in
its dependency array. Since these functions are recreated on every render,
this caused an infinite loop of RPC calls when refreshing the primary
session, resulting in 100+ identical getLocalVersion requests.

Fix: Remove function references from dependency array, only keep appVersion
which is the actual data dependency. The effect now only runs once when
appVersion changes from null to a value.

This is the same pattern as the previous fix for getVideoState,
getKeyboardLedState, and getKeyDownState.
The getPermissions useEffect had send and pollPermissions in its dependency
array. Since send gets recreated when rpcDataChannel changes, this caused
multiple getPermissions RPC calls (5 observed) on page load.

Fix:
- Add rpcDataChannel readiness check to prevent calls before channel is open
- Remove send and pollPermissions from dependency array
- Keep only currentMode and rpcDataChannel.readyState as dependencies

This ensures getPermissions is called only when:
1. The RPC channel becomes ready (readyState changes to "open")
2. The session mode changes (observer <-> primary)

Eliminates duplicate RPC calls while maintaining correct behavior for
mode changes and initial connection.
…undant code

Improvements:
- Centralized permission state management in PermissionsProvider
  - Eliminates duplicate RPC calls across components
  - Single source of truth for permission state
  - Automatic HID re-initialization on permission changes
- Split exports into separate files for React Fast Refresh compliance
  - Created types/permissions.ts for Permission enum
  - Created hooks/usePermissions.ts for the hook with safe defaults
  - Created contexts/PermissionsContext.ts for context definition
  - Updated PermissionsProvider.tsx to only export the provider component
- Removed redundant getSessionSettings RPC call (settings already in WebSocket/WebRTC messages)
- Added connectionModeChanged event handler for seamless emergency promotions
- Fixed approval dialog race condition by checking isLoadingPermissions
- Removed all redundant comments and code for leaner implementation
- Updated imports across 10+ component files

Result: Zero ESLint warnings, cleaner architecture, no duplicate RPC calls, all functionality preserved
Issue:
- RPC calls (getVideoState, getKeyboardLedState, getKeyDownState) were being made
  immediately when RPC data channel opened, before permissions were granted
- This caused "Permission denied" errors in console for pending/queued sessions
- Sessions waiting for nickname or approval were triggering permission errors

Solution:
- Added currentMode checks to guard RPC initialization calls
- Only make RPC calls when session is in "primary" or "observer" mode
- Skip RPC calls for "pending" or "queued" sessions

Result: No more permission errors before session approval
Replace UI-state based guards (showNicknameModal, currentMode checks) with
actual permission checks from PermissionsProvider. This ensures RPC calls
are only made when sessions have the required permissions.

Changes:
- getVideoState now checks for Permission.VIDEO_VIEW
- getKeyboardLedState checks for Permission.KEYBOARD_INPUT
- getKeyDownState checks for Permission.KEYBOARD_INPUT
- All checks wait for permissions to load (isLoadingPermissions)

This prevents "Permission denied" errors that occurred when RPC calls
were made before sessions received proper permissions.
pennycoders and others added 16 commits October 17, 2025 10:09
Extract large switch statements and functions into focused, reusable handlers
to improve code maintainability while preserving 100% functionality.

Changes:
- Extract onRPCMessage switch (200+ lines → 20 lines) into jsonrpc_session_handlers.go
- Extract cleanupInactiveSessions (343 lines → 54 lines) into session_cleanup_handlers.go
- Consolidate duplicate emergency promotion logic into attemptEmergencyPromotion()
- Simplify shouldBecomePrimary boolean logic with self-documenting variables

All changes pass linting (0 issues) and maintain complete functionality.
Remove int→int16 type signature changes from internal/usbgadget/ that were
not essential to multi-session functionality. These changes should be part
of a separate USB improvement PR.

Changes:
- Revert AbsMouseReport signature to use int instead of int16
- Remove int16 casts in hidrpc.go calling code
- Update usb.go wrapper functions to match

This keeps the multi-session PR focused on session management without
coupling unrelated USB gadget refactoring.
…ssion

This commit resolves multiple critical issues in the multi-session implementation:

Race Conditions Fixed:
- Add primaryPromotionLock mutex to prevent dual-primary corruption
- Implement atomic nickname reservation before session addition
- Add corruption detection and auto-fix in transferPrimaryRole
- Implement broadcast coalescing to prevent storms

Security Improvements:
- Add permission check for HID RPC handshake
- Implement sliding window rate limiting for emergency promotions
- Add global RPC rate limiter (2000 req/sec across all sessions)
- Enhance nickname validation (control chars, zero-width chars, unicode)

Reliability Enhancements:
- Add 5-second timeouts to all WebSocket writes
- Add RPC queue monitoring (warns at 200+ messages)
- Verify grace period memory leak protection
- Verify goroutine cleanup on session removal

Technical Details:
- Use double-locking pattern (primaryPromotionLock → mu)
- Implement deferred cleanup for failed nickname reservations
- Use atomic.Bool for broadcast coalescing
- Add trust scoring for emergency promotion selection

Files Modified:
- session_manager.go: Core session management fixes
- session_cleanup_handlers.go: Rate limiting for emergency promotions
- hidrpc.go: Permission checks for handshake
- jsonrpc_session_handlers.go: Enhanced nickname validation
- jsonrpc.go: Global RPC rate limiting
- webrtc.go: WebSocket timeouts and queue monitoring

Total: 266 insertions, 73 deletions across 6 files
Fixed critical bug where primary session timeout was never triggered even
after configured inactivity period (e.g., 60 seconds with no input).

Root cause: LastActive timestamp was being reset during WebSocket
reconnections and session promotions, preventing the inactivity timer
from ever reaching the timeout threshold.

Changes:
- session_manager.go:245: Removed LastActive reset during reconnection
  in AddSession(). Reconnections should NOT reset the activity timer
  since timeout is based on input activity, not connection activity.

- session_manager.go:1207-1209: Made LastActive reset conditional in
  transferPrimaryRole(). Only emergency promotions reset the timer to
  prevent immediate re-timeout. Manual transfers preserve existing
  LastActive for accurate timeout tracking.

Impact:
- Primary sessions will now correctly timeout after configured inactivity
- LastActive only updated by actual user input (keyboard/mouse events)
- Emergency promotions still get fresh timer to prevent rapid re-timeout
- Manual transfers maintain accurate activity tracking

Test scenario:
1. User becomes primary and leaves tab in background
2. No keyboard/mouse input for 60+ seconds (timeout configured)
3. WebSocket stays connected but LastActive is not reset
4. handlePrimarySessionTimeout() detects inactivity and demotes primary
5. Next eligible observer is automatically promoted
Problem: The jiggler was calling sessionManager.UpdateLastActive() which
prevented the primary session timeout from ever triggering. This made it
impossible to automatically demote inactive primary sessions.

Root cause analysis:
- Jiggler is automated mouse movement to prevent remote PC sleep
- It was incorrectly updating LastActive timestamp as if it were user input
- This reset the inactivity timer every time jiggler ran
- Primary session timeout requires LastActive to remain unchanged during
  actual user inactivity

Changes:
- Removed sessionManager.UpdateLastActive() call from jiggler.go:145
- Added comment explaining why jiggler should not update LastActive
- Session timeout now correctly tracks only REAL user input:
  * Keyboard events (via USB HID)
  * Mouse events (via USB HID)
  * Native operations
- Jiggler mouse movement is explicitly excluded from activity tracking

This works together with the previous fix that removed LastActive reset
during WebSocket reconnections.

Impact:
- Primary sessions will now correctly timeout after configured inactivity
- Jiggler continues to prevent remote PC sleep as intended
- Only genuine user input resets the inactivity timer

Test:
1. Enable jiggler with short interval (e.g., every 10 seconds)
2. Set primary timeout to 60 seconds
3. Leave primary tab in background with no user input
4. Jiggler will keep remote PC awake
5. After 60 seconds, primary session is correctly demoted
Replaced custom jiggler implementation with dev branch version:
- Uses rpcAbsMouseReport() instead of gadget.RelMouseReport()
- Maintains same behavior: does NOT call UpdateLastActive()
- Ensures jiggler activity doesn't interfere with session timeouts
- Preserves all multi-session timeout fixes

This change does not affect multi-session functionality.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Fix nickname index stale pointer during session reconnection
- Reset LastActive for all emergency promotions to prevent cascade timeouts
- Bypass rate limits when no primary exists to prevent system deadlock
- Replace manual mutex with atomic.Int32 for session counter (fixes race condition)
- Implement collect-then-delete pattern for safe map iteration
- Reduce logging verbosity for routine cleanup operations
Integrate upstream changes from dev branch including power saving feature
and video improvements. Resolved conflict in hardware settings by merging
permission checks with new power saving controls.

Changes from dev:
- Add HDMI sleep mode power saving feature
- Video capture improvements
- Native interface updates

Multi-session features preserved:
- Permission-based settings access control
- All session management functionality intact
Moved getVideoSleepMode useEffect before early returns to comply with
React Rules of Hooks. All hooks must be called in the same order on
every component render, before any conditional returns.

This completes the merge from dev branch, preserving both:
- Permission-based access control from multi-session branch
- HDMI sleep mode power saving feature from dev branch
The getLocalVersion() call was happening before the WebRTC RPC data
channel was established, causing version fetch to fail silently.

This resulted in:
- Feature flags always returning false (no version = disabled)
- Settings UI showing "App: Loading..." and "System: Loading..." indefinitely
- Version-gated features (like HDMI Sleep Mode) never appearing

Fixed by adding rpcDataChannel readyState check before calling
getLocalVersion(), matching the pattern used by all other RPC calls
in the same file.
Changed from immediate assignment (:=) to conditional assignment (?=)
to allow developers to override version numbers when building.

This enables building with custom versions for testing:
  VERSION=0.4.9 ./dev_deploy.sh -r <ip> --install

Useful for testing version-gated features without bumping the
default production version.
@pennycoders pennycoders marked this pull request as ready for review October 22, 2025 22:28
…ment

- Fix panic recovery in AddSession to log instead of re-throwing, preventing process crashes
- Fix integer overflow in trust score calculation by capping before int conversion
- Fix TOCTOU race condition in nickname uniqueness check with atomic UpdateSessionNickname method
The primary session inactivity timeout was only tracking HID-related
activity (keyboard, mouse, mass storage) but not general RPC messages.
This meant users watching video or interacting with the UI would be
timed out even though they were actively using the session.

Added UpdateLastActive call in onRPCMessage to track any valid RPC
activity, ensuring all user interactions reset the inactivity timer.
When primary timed out, emergency promotion was re-promoting the same
timed-out session instead of promoting an observer. The emergency bypass
logic ignored the excludeSessionID parameter.

Fixed by applying session exclusion logic in all emergency promotion paths.
- Fix dual-primary race condition in reconnection logic by verifying actual session mode
- Fix authentication bypass in cloud registration endpoint
- Fix grace period eviction DoS by preventing loop and adding defensive checks
- Improve RPC error logging to distinguish closed channels from actual errors
- Remove LastActive reset on emergency promotion to preserve actual activity state
- Fix UI rejection count race by tracking whether rejection was already counted
- Optimize browser detection to avoid redundant string searches
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants