Skip to content

Commit afb146d

Browse files
ymadamshiervani
andauthored
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 <[email protected]>
1 parent 72e3013 commit afb146d

File tree

18 files changed

+749
-280
lines changed

18 files changed

+749
-280
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/gin-contrib/logger v1.2.6
1313
github.com/gin-gonic/gin v1.10.1
1414
github.com/go-co-op/gocron/v2 v2.16.5
15+
github.com/google/flatbuffers v25.2.10+incompatible
1516
github.com/google/uuid v1.6.0
1617
github.com/guregu/null/v6 v6.0.0
1718
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f
@@ -23,6 +24,7 @@ require (
2324
github.com/prometheus/common v0.66.0
2425
github.com/prometheus/procfs v0.17.0
2526
github.com/psanford/httpreadat v0.1.0
27+
github.com/rs/xid v1.6.0
2628
github.com/rs/zerolog v1.34.0
2729
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f
2830
github.com/stretchr/testify v1.11.1

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAu
5353
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
5454
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
5555
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
56+
github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
57+
github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
5658
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
5759
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
5860
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -152,6 +154,7 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
152154
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
153155
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
154156
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
157+
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
155158
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
156159
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
157160
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=

hidrpc.go

Lines changed: 90 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/jetkvm/kvm/internal/hidrpc"
1010
"github.com/jetkvm/kvm/internal/usbgadget"
11+
"github.com/rs/zerolog"
1112
)
1213

1314
func handleHidRPCMessage(message hidrpc.Message, session *Session) {
@@ -26,21 +27,19 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
2627
}
2728
session.hidRPCAvailable = true
2829
case hidrpc.TypeKeypressReport, hidrpc.TypeKeyboardReport:
29-
keysDownState, err := handleHidRPCKeyboardInput(message)
30-
if keysDownState != nil {
31-
session.reportHidRPCKeysDownState(*keysDownState)
32-
}
33-
rpcErr = err
30+
rpcErr = handleHidRPCKeyboardInput(message)
3431
case hidrpc.TypeKeyboardMacroReport:
3532
keyboardMacroReport, err := message.KeyboardMacroReport()
3633
if err != nil {
3734
logger.Warn().Err(err).Msg("failed to get keyboard macro report")
3835
return
3936
}
40-
_, rpcErr = rpcExecuteKeyboardMacro(keyboardMacroReport.Steps)
37+
rpcErr = rpcExecuteKeyboardMacro(keyboardMacroReport.Steps)
4138
case hidrpc.TypeCancelKeyboardMacroReport:
4239
rpcCancelKeyboardMacro()
4340
return
41+
case hidrpc.TypeKeypressKeepAliveReport:
42+
rpcErr = handleHidRPCKeypressKeepAlive(session)
4443
case hidrpc.TypePointerReport:
4544
pointerReport, err := message.PointerReport()
4645
if err != nil {
@@ -64,8 +63,13 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
6463
}
6564
}
6665

67-
func onHidMessage(data []byte, session *Session) {
68-
scopedLogger := hidRPCLogger.With().Bytes("data", data).Logger()
66+
func onHidMessage(msg hidQueueMessage, session *Session) {
67+
data := msg.Data
68+
69+
scopedLogger := hidRPCLogger.With().
70+
Str("channel", msg.channel).
71+
Bytes("data", data).
72+
Logger()
6973
scopedLogger.Debug().Msg("HID RPC message received")
7074

7175
if len(data) < 1 {
@@ -80,7 +84,9 @@ func onHidMessage(data []byte, session *Session) {
8084
return
8185
}
8286

83-
scopedLogger = scopedLogger.With().Str("descr", message.String()).Logger()
87+
if scopedLogger.GetLevel() <= zerolog.DebugLevel {
88+
scopedLogger = scopedLogger.With().Str("descr", message.String()).Logger()
89+
}
8490

8591
t := time.Now()
8692

@@ -97,27 +103,88 @@ func onHidMessage(data []byte, session *Session) {
97103
}
98104
}
99105

100-
func handleHidRPCKeyboardInput(message hidrpc.Message) (*usbgadget.KeysDownState, error) {
106+
// Tunables
107+
// Keep in mind
108+
// macOS default: 15 * 15 = 225ms https://discussions.apple.com/thread/1316947?sortBy=rank
109+
// Linux default: 250ms https://man.archlinux.org/man/kbdrate.8.en
110+
// Windows default: 1s `HKEY_CURRENT_USER\Control Panel\Accessibility\Keyboard Response\AutoRepeatDelay`
111+
112+
const expectedRate = 50 * time.Millisecond // expected keepalive interval
113+
const maxLateness = 50 * time.Millisecond // max jitter we'll tolerate OR jitter budget
114+
const baseExtension = expectedRate + maxLateness // 100ms extension on perfect tick
115+
116+
const maxStaleness = 225 * time.Millisecond // discard ancient packets outright
117+
118+
func handleHidRPCKeypressKeepAlive(session *Session) error {
119+
session.keepAliveJitterLock.Lock()
120+
defer session.keepAliveJitterLock.Unlock()
121+
122+
now := time.Now()
123+
124+
// 1) Staleness guard: ensures packets that arrive far beyond the life of a valid key hold
125+
// (e.g. after a network stall, retransmit burst, or machine sleep) are ignored outright.
126+
// This prevents “zombie” keepalives from reviving a key that should already be released.
127+
if !session.lastTimerResetTime.IsZero() && now.Sub(session.lastTimerResetTime) > maxStaleness {
128+
return nil
129+
}
130+
131+
validTick := true
132+
timerExtension := baseExtension
133+
134+
if !session.lastKeepAliveArrivalTime.IsZero() {
135+
timeSinceLastTick := now.Sub(session.lastKeepAliveArrivalTime)
136+
lateness := timeSinceLastTick - expectedRate
137+
138+
if lateness > 0 {
139+
if lateness <= maxLateness {
140+
// --- Small lateness (within jitterBudget) ---
141+
// This is normal jitter (e.g., Wi-Fi contention).
142+
// We still accept the tick, but *reduce the extension*
143+
// so that the total hold time stays aligned with REAL client side intent.
144+
timerExtension -= lateness
145+
} else {
146+
// --- Large lateness (beyond jitterBudget) ---
147+
// This is likely a retransmit stall or ordering delay.
148+
// We reject the tick entirely and DO NOT extend,
149+
// so the auto-release still fires on time.
150+
validTick = false
151+
}
152+
}
153+
}
154+
155+
if !validTick {
156+
return nil
157+
}
158+
// Only valid ticks update our state and extend the timer.
159+
session.lastKeepAliveArrivalTime = now
160+
session.lastTimerResetTime = now
161+
if gadget != nil {
162+
gadget.DelayAutoReleaseWithDuration(timerExtension)
163+
}
164+
165+
// On a miss: do not advance any state — keeps baseline stable.
166+
return nil
167+
}
168+
169+
func handleHidRPCKeyboardInput(message hidrpc.Message) error {
101170
switch message.Type() {
102171
case hidrpc.TypeKeypressReport:
103172
keypressReport, err := message.KeypressReport()
104173
if err != nil {
105174
logger.Warn().Err(err).Msg("failed to get keypress report")
106-
return nil, err
175+
return err
107176
}
108-
keysDownState, rpcError := rpcKeypressReport(keypressReport.Key, keypressReport.Press)
109-
return &keysDownState, rpcError
177+
return rpcKeypressReport(keypressReport.Key, keypressReport.Press)
110178
case hidrpc.TypeKeyboardReport:
111179
keyboardReport, err := message.KeyboardReport()
112180
if err != nil {
113181
logger.Warn().Err(err).Msg("failed to get keyboard report")
114-
return nil, err
182+
return err
115183
}
116-
keysDownState, rpcError := rpcKeyboardReport(keyboardReport.Modifier, keyboardReport.Keys)
117-
return &keysDownState, rpcError
184+
return rpcKeyboardReport(keyboardReport.Modifier, keyboardReport.Keys)
118185
}
119186

120-
return nil, fmt.Errorf("unknown HID RPC message type: %d", message.Type())
187+
return fmt.Errorf("unknown HID RPC message type: %d", message.Type())
121188
}
122189

123190
func reportHidRPC(params any, session *Session) {
@@ -127,7 +194,10 @@ func reportHidRPC(params any, session *Session) {
127194
}
128195

129196
if !session.hidRPCAvailable || session.HidChannel == nil {
130-
logger.Warn().Msg("HID RPC is not available, skipping reportHidRPC")
197+
logger.Warn().
198+
Bool("hidRPCAvailable", session.hidRPCAvailable).
199+
Bool("HidChannel", session.HidChannel != nil).
200+
Msg("HID RPC is not available, skipping reportHidRPC")
131201
return
132202
}
133203

@@ -174,8 +244,10 @@ func (s *Session) reportHidRPCKeyboardLedState(state usbgadget.KeyboardState) {
174244

175245
func (s *Session) reportHidRPCKeysDownState(state usbgadget.KeysDownState) {
176246
if !s.hidRPCAvailable {
247+
usbLogger.Debug().Interface("state", state).Msg("reporting keys down state")
177248
writeJSONRPCEvent("keysDownState", state, s)
178249
}
250+
usbLogger.Debug().Interface("state", state).Msg("reporting keys down state, calling reportHidRPC")
179251
reportHidRPC(state, s)
180252
}
181253

internal/hidrpc/hidrpc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const (
1515
TypePointerReport MessageType = 0x03
1616
TypeWheelReport MessageType = 0x04
1717
TypeKeypressReport MessageType = 0x05
18+
TypeKeypressKeepAliveReport MessageType = 0x09
1819
TypeMouseReport MessageType = 0x06
1920
TypeKeyboardMacroReport MessageType = 0x07
2021
TypeCancelKeyboardMacroReport MessageType = 0x08

internal/hidrpc/message.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ func (m *Message) String() string {
4444
return fmt.Sprintf("MouseReport{Malformed: %v}", m.d)
4545
}
4646
return fmt.Sprintf("MouseReport{DX: %d, DY: %d, Button: %d}", m.d[0], m.d[1], m.d[2])
47+
case TypeKeypressKeepAliveReport:
48+
return "KeypressKeepAliveReport"
4749
case TypeKeyboardMacroReport:
4850
if len(m.d) < 5 {
4951
return fmt.Sprintf("KeyboardMacroReport{Malformed: %v}", m.d)

0 commit comments

Comments
 (0)