feat: release keyPress automatically (#796)

* feat: release keyPress automatically

* send keepalive when pressing the key

* remove logging

* clean up logging

* chore: use unreliable channel to send keepalive events

* chore: use ordered unreliable channel for pointer events

* chore: adjust auto release key interval

* chore: update logging for kbdAutoReleaseLock

* chore: update comment for KEEPALIVE_INTERVAL

* fix: should cancelAutorelease when pressed is true

* fix: handshake won't happen if webrtc reconnects

* chore: add trace log for writeWithTimeout

* chore: add timeout for KeypressReport

* chore: use the proper key to send release command

* refactor: simplify HID RPC keyboard input handling and improve key state management

- Updated `handleHidRPCKeyboardInput` to return errors directly instead of keys down state.
- Refactored `rpcKeyboardReport` and `rpcKeypressReport` to return errors instead of states.
- Introduced a queue for managing key down state updates in the `Session` struct to prevent input handling stalls.
- Adjusted the `UpdateKeysDown` method to handle state changes more efficiently.
- Removed unnecessary logging and commented-out code for clarity.

* refactor: enhance keyboard auto-release functionality and key state management

* fix: correct Windows default auto-repeat delay comment from 1ms to 1s

* refactor: send keypress as early as possible

* refactor: replace console.warn with console.info for HID RPC channel events

* refactor: remove unused NewKeypressKeepAliveMessage function from HID RPC

* fix: handle error in key release process and log warnings

* fix: log warning on keypress report failure

* fix: update auto-release keyboard interval to 225

* refactor: enhance keep-alive handling and jitter compensation in HID RPC

- Implemented staleness guard to ignore outdated keep-alive packets.
- Added jitter compensation logic to adjust timer extensions based on packet arrival times.
- Introduced new methods for managing keep-alive state and reset functionality in the Session struct.
- Updated auto-release delay mechanism to use dynamic durations based on keep-alive timing.
- Adjusted keep-alive interval in the UI to improve responsiveness.

* gofmt

* clean up code

* chore: use dynamic duration for scheduleAutoRelease

* Use harcoded timer reset value for now

* fix: prevent nil pointer dereference when stopping timers in Close method

* refactor: remove nil check for kbdAutoReleaseTimers in DelayAutoReleaseWithDuration

* refactor: optimize dependencies in useHidRpc hooks

* refactor: streamline keep-alive timer management in useKeyboard hook

* refactor: clarify comments in useKeyboard hook for resetKeyboardState function

* refactor: reduce keysDownStateQueueSize

* refactor: close and reset keysDownStateQueue in newSession function

* chore: resolve conflicts

* resolve conflicts

---------

Co-authored-by: Adam Shiervani <adam.shiervani@gmail.com>
This commit is contained in:
Aveline
2025-09-18 13:35:47 +02:00
committed by GitHub
parent 72e3013337
commit afb146d78c
18 changed files with 767 additions and 298 deletions

144
webrtc.go
View File

@@ -7,12 +7,14 @@ import (
"net"
"strings"
"sync"
"time"
"github.com/coder/websocket"
"github.com/coder/websocket/wsjson"
"github.com/gin-gonic/gin"
"github.com/jetkvm/kvm/internal/hidrpc"
"github.com/jetkvm/kvm/internal/logging"
"github.com/jetkvm/kvm/internal/usbgadget"
"github.com/pion/webrtc/v4"
"github.com/rs/zerolog"
)
@@ -27,9 +29,26 @@ type Session struct {
rpcQueue chan webrtc.DataChannelMessage
hidRPCAvailable bool
hidQueueLock sync.Mutex
hidQueue []chan webrtc.DataChannelMessage
hidRPCAvailable bool
lastKeepAliveArrivalTime time.Time // Track when last keep-alive packet arrived
lastTimerResetTime time.Time // Track when auto-release timer was last reset
keepAliveJitterLock sync.Mutex // Protect jitter compensation timing state
hidQueueLock sync.Mutex
hidQueue []chan hidQueueMessage
keysDownStateQueue chan usbgadget.KeysDownState
}
func (s *Session) resetKeepAliveTime() {
s.keepAliveJitterLock.Lock()
defer s.keepAliveJitterLock.Unlock()
s.lastKeepAliveArrivalTime = time.Time{} // Reset keep-alive timing tracking
s.lastTimerResetTime = time.Time{} // Reset auto-release timer tracking
}
type hidQueueMessage struct {
webrtc.DataChannelMessage
channel string
}
type SessionConfig struct {
@@ -78,16 +97,85 @@ func (s *Session) initQueues() {
s.hidQueueLock.Lock()
defer s.hidQueueLock.Unlock()
s.hidQueue = make([]chan webrtc.DataChannelMessage, 0)
s.hidQueue = make([]chan hidQueueMessage, 0)
for i := 0; i < 4; i++ {
q := make(chan webrtc.DataChannelMessage, 256)
q := make(chan hidQueueMessage, 256)
s.hidQueue = append(s.hidQueue, q)
}
}
func (s *Session) handleQueues(index int) {
for msg := range s.hidQueue[index] {
onHidMessage(msg.Data, s)
onHidMessage(msg, s)
}
}
const keysDownStateQueueSize = 64
func (s *Session) initKeysDownStateQueue() {
// serialise outbound key state reports so unreliable links can't stall input handling
s.keysDownStateQueue = make(chan usbgadget.KeysDownState, keysDownStateQueueSize)
go s.handleKeysDownStateQueue()
}
func (s *Session) handleKeysDownStateQueue() {
for state := range s.keysDownStateQueue {
s.reportHidRPCKeysDownState(state)
}
}
func (s *Session) enqueueKeysDownState(state usbgadget.KeysDownState) {
if s == nil || s.keysDownStateQueue == nil {
return
}
select {
case s.keysDownStateQueue <- state:
default:
hidRPCLogger.Warn().Msg("dropping keys down state update; queue full")
}
}
func getOnHidMessageHandler(session *Session, scopedLogger *zerolog.Logger, channel string) func(msg webrtc.DataChannelMessage) {
return func(msg webrtc.DataChannelMessage) {
l := scopedLogger.With().
Str("channel", channel).
Int("length", len(msg.Data)).
Logger()
// only log data if the log level is debug or lower
if scopedLogger.GetLevel() > zerolog.DebugLevel {
l = l.With().Str("data", string(msg.Data)).Logger()
}
if msg.IsString {
l.Warn().Msg("received string data in HID RPC message handler")
return
}
if len(msg.Data) < 1 {
l.Warn().Msg("received empty data in HID RPC message handler")
return
}
l.Trace().Msg("received data in HID RPC message handler")
// Enqueue to ensure ordered processing
queueIndex := hidrpc.GetQueueIndex(hidrpc.MessageType(msg.Data[0]))
if queueIndex >= len(session.hidQueue) || queueIndex < 0 {
l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue index not found")
queueIndex = 3
}
queue := session.hidQueue[queueIndex]
if queue != nil {
queue <- hidQueueMessage{
DataChannelMessage: msg,
channel: channel,
}
} else {
l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue is nil")
return
}
}
}
@@ -133,6 +221,7 @@ func newSession(config SessionConfig) (*Session, error) {
session := &Session{peerConnection: peerConnection}
session.rpcQueue = make(chan webrtc.DataChannelMessage, 256)
session.initQueues()
session.initKeysDownStateQueue()
go func() {
for msg := range session.rpcQueue {
@@ -157,40 +246,12 @@ func newSession(config SessionConfig) (*Session, error) {
switch d.Label() {
case "hidrpc":
session.HidChannel = d
d.OnMessage(func(msg webrtc.DataChannelMessage) {
l := scopedLogger.With().Int("length", len(msg.Data)).Logger()
// only log data if the log level is debug or lower
if scopedLogger.GetLevel() > zerolog.DebugLevel {
l = l.With().Str("data", string(msg.Data)).Logger()
}
if msg.IsString {
l.Warn().Msg("received string data in HID RPC message handler")
return
}
if len(msg.Data) < 1 {
l.Warn().Msg("received empty data in HID RPC message handler")
return
}
l.Trace().Msg("received data in HID RPC message handler")
// Enqueue to ensure ordered processing
queueIndex := hidrpc.GetQueueIndex(hidrpc.MessageType(msg.Data[0]))
if queueIndex >= len(session.hidQueue) || queueIndex < 0 {
l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue index not found")
queueIndex = 3
}
queue := session.hidQueue[queueIndex]
if queue != nil {
queue <- msg
} else {
l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue is nil")
return
}
})
d.OnMessage(getOnHidMessageHandler(session, scopedLogger, "hidrpc"))
// we won't send anything over the unreliable channels
case "hidrpc-unreliable-ordered":
d.OnMessage(getOnHidMessageHandler(session, scopedLogger, "hidrpc-unreliable-ordered"))
case "hidrpc-unreliable-nonordered":
d.OnMessage(getOnHidMessageHandler(session, scopedLogger, "hidrpc-unreliable-nonordered"))
case "rpc":
session.RPCChannel = d
d.OnMessage(func(msg webrtc.DataChannelMessage) {
@@ -282,6 +343,9 @@ func newSession(config SessionConfig) (*Session, error) {
session.hidQueue[i] = nil
}
close(session.keysDownStateQueue)
session.keysDownStateQueue = nil
if session.shouldUmountVirtualMedia {
if err := rpcUnmountImage(); err != nil {
scopedLogger.Warn().Err(err).Msg("unmount image failed on connection close")