mirror of https://github.com/jetkvm/kvm.git
260 lines
7.7 KiB
Go
260 lines
7.7 KiB
Go
package kvm
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/jetkvm/kvm/internal/hidrpc"
|
|
"github.com/jetkvm/kvm/internal/usbgadget"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
func handleHidRPCMessage(message hidrpc.Message, session *Session) {
|
|
var rpcErr error
|
|
|
|
switch message.Type() {
|
|
case hidrpc.TypeHandshake:
|
|
message, err := hidrpc.NewHandshakeMessage().Marshal()
|
|
if err != nil {
|
|
logger.Warn().Err(err).Msg("failed to marshal handshake message")
|
|
return
|
|
}
|
|
if err := session.HidChannel.Send(message); err != nil {
|
|
logger.Warn().Err(err).Msg("failed to send handshake message")
|
|
return
|
|
}
|
|
session.hidRPCAvailable = true
|
|
case hidrpc.TypeKeypressReport, hidrpc.TypeKeyboardReport:
|
|
rpcErr = handleHidRPCKeyboardInput(message)
|
|
case hidrpc.TypeKeyboardMacroReport:
|
|
keyboardMacroReport, err := message.KeyboardMacroReport()
|
|
if err != nil {
|
|
logger.Warn().Err(err).Msg("failed to get keyboard macro report")
|
|
return
|
|
}
|
|
rpcErr = rpcExecuteKeyboardMacro(keyboardMacroReport.Steps)
|
|
case hidrpc.TypeCancelKeyboardMacroReport:
|
|
rpcCancelKeyboardMacro()
|
|
return
|
|
case hidrpc.TypeKeypressKeepAliveReport:
|
|
rpcErr = handleHidRPCKeypressKeepAlive(session)
|
|
case hidrpc.TypePointerReport:
|
|
pointerReport, err := message.PointerReport()
|
|
if err != nil {
|
|
logger.Warn().Err(err).Msg("failed to get pointer report")
|
|
return
|
|
}
|
|
rpcErr = rpcAbsMouseReport(pointerReport.X, pointerReport.Y, pointerReport.Button)
|
|
case hidrpc.TypeMouseReport:
|
|
mouseReport, err := message.MouseReport()
|
|
if err != nil {
|
|
logger.Warn().Err(err).Msg("failed to get mouse report")
|
|
return
|
|
}
|
|
rpcErr = rpcRelMouseReport(mouseReport.DX, mouseReport.DY, mouseReport.Button)
|
|
default:
|
|
logger.Warn().Uint8("type", uint8(message.Type())).Msg("unknown HID RPC message type")
|
|
}
|
|
|
|
if rpcErr != nil {
|
|
logger.Warn().Err(rpcErr).Msg("failed to handle HID RPC message")
|
|
}
|
|
}
|
|
|
|
func onHidMessage(msg hidQueueMessage, session *Session) {
|
|
data := msg.Data
|
|
|
|
scopedLogger := hidRPCLogger.With().
|
|
Str("channel", msg.channel).
|
|
Bytes("data", data).
|
|
Logger()
|
|
scopedLogger.Debug().Msg("HID RPC message received")
|
|
|
|
if len(data) < 1 {
|
|
scopedLogger.Warn().Int("length", len(data)).Msg("received empty data in HID RPC message handler")
|
|
return
|
|
}
|
|
|
|
var message hidrpc.Message
|
|
|
|
if err := hidrpc.Unmarshal(data, &message); err != nil {
|
|
scopedLogger.Warn().Err(err).Msg("failed to unmarshal HID RPC message")
|
|
return
|
|
}
|
|
|
|
if scopedLogger.GetLevel() <= zerolog.DebugLevel {
|
|
scopedLogger = scopedLogger.With().Str("descr", message.String()).Logger()
|
|
}
|
|
|
|
t := time.Now()
|
|
|
|
r := make(chan interface{})
|
|
go func() {
|
|
handleHidRPCMessage(message, session)
|
|
r <- nil
|
|
}()
|
|
select {
|
|
case <-time.After(1 * time.Second):
|
|
scopedLogger.Warn().Msg("HID RPC message timed out")
|
|
case <-r:
|
|
scopedLogger.Debug().Dur("duration", time.Since(t)).Msg("HID RPC message handled")
|
|
}
|
|
}
|
|
|
|
// Tunables
|
|
// Keep in mind
|
|
// macOS default: 15 * 15 = 225ms https://discussions.apple.com/thread/1316947?sortBy=rank
|
|
// Linux default: 250ms https://man.archlinux.org/man/kbdrate.8.en
|
|
// Windows default: 1s `HKEY_CURRENT_USER\Control Panel\Accessibility\Keyboard Response\AutoRepeatDelay`
|
|
|
|
const expectedRate = 50 * time.Millisecond // expected keepalive interval
|
|
const maxLateness = 50 * time.Millisecond // max jitter we'll tolerate OR jitter budget
|
|
const baseExtension = expectedRate + maxLateness // 100ms extension on perfect tick
|
|
|
|
const maxStaleness = 225 * time.Millisecond // discard ancient packets outright
|
|
|
|
func handleHidRPCKeypressKeepAlive(session *Session) error {
|
|
session.keepAliveJitterLock.Lock()
|
|
defer session.keepAliveJitterLock.Unlock()
|
|
|
|
now := time.Now()
|
|
|
|
// 1) Staleness guard: ensures packets that arrive far beyond the life of a valid key hold
|
|
// (e.g. after a network stall, retransmit burst, or machine sleep) are ignored outright.
|
|
// This prevents “zombie” keepalives from reviving a key that should already be released.
|
|
if !session.lastTimerResetTime.IsZero() && now.Sub(session.lastTimerResetTime) > maxStaleness {
|
|
return nil
|
|
}
|
|
|
|
validTick := true
|
|
timerExtension := baseExtension
|
|
|
|
if !session.lastKeepAliveArrivalTime.IsZero() {
|
|
timeSinceLastTick := now.Sub(session.lastKeepAliveArrivalTime)
|
|
lateness := timeSinceLastTick - expectedRate
|
|
|
|
if lateness > 0 {
|
|
if lateness <= maxLateness {
|
|
// --- Small lateness (within jitterBudget) ---
|
|
// This is normal jitter (e.g., Wi-Fi contention).
|
|
// We still accept the tick, but *reduce the extension*
|
|
// so that the total hold time stays aligned with REAL client side intent.
|
|
timerExtension -= lateness
|
|
} else {
|
|
// --- Large lateness (beyond jitterBudget) ---
|
|
// This is likely a retransmit stall or ordering delay.
|
|
// We reject the tick entirely and DO NOT extend,
|
|
// so the auto-release still fires on time.
|
|
validTick = false
|
|
}
|
|
}
|
|
}
|
|
|
|
if !validTick {
|
|
return nil
|
|
}
|
|
// Only valid ticks update our state and extend the timer.
|
|
session.lastKeepAliveArrivalTime = now
|
|
session.lastTimerResetTime = now
|
|
if gadget != nil {
|
|
gadget.DelayAutoReleaseWithDuration(timerExtension)
|
|
}
|
|
|
|
// On a miss: do not advance any state — keeps baseline stable.
|
|
return nil
|
|
}
|
|
|
|
func handleHidRPCKeyboardInput(message hidrpc.Message) error {
|
|
switch message.Type() {
|
|
case hidrpc.TypeKeypressReport:
|
|
keypressReport, err := message.KeypressReport()
|
|
if err != nil {
|
|
logger.Warn().Err(err).Msg("failed to get keypress report")
|
|
return err
|
|
}
|
|
return rpcKeypressReport(keypressReport.Key, keypressReport.Press)
|
|
case hidrpc.TypeKeyboardReport:
|
|
keyboardReport, err := message.KeyboardReport()
|
|
if err != nil {
|
|
logger.Warn().Err(err).Msg("failed to get keyboard report")
|
|
return err
|
|
}
|
|
return rpcKeyboardReport(keyboardReport.Modifier, keyboardReport.Keys)
|
|
}
|
|
|
|
return fmt.Errorf("unknown HID RPC message type: %d", message.Type())
|
|
}
|
|
|
|
func reportHidRPC(params any, session *Session) {
|
|
if session == nil {
|
|
logger.Warn().Msg("session is nil, skipping reportHidRPC")
|
|
return
|
|
}
|
|
|
|
if !session.hidRPCAvailable || session.HidChannel == nil {
|
|
logger.Warn().
|
|
Bool("hidRPCAvailable", session.hidRPCAvailable).
|
|
Bool("HidChannel", session.HidChannel != nil).
|
|
Msg("HID RPC is not available, skipping reportHidRPC")
|
|
return
|
|
}
|
|
|
|
var (
|
|
message []byte
|
|
err error
|
|
)
|
|
switch params := params.(type) {
|
|
case usbgadget.KeyboardState:
|
|
message, err = hidrpc.NewKeyboardLedMessage(params).Marshal()
|
|
case usbgadget.KeysDownState:
|
|
message, err = hidrpc.NewKeydownStateMessage(params).Marshal()
|
|
case hidrpc.KeyboardMacroState:
|
|
message, err = hidrpc.NewKeyboardMacroStateMessage(params.State, params.IsPaste).Marshal()
|
|
default:
|
|
err = fmt.Errorf("unknown HID RPC message type: %T", params)
|
|
}
|
|
|
|
if err != nil {
|
|
logger.Warn().Err(err).Msg("failed to marshal HID RPC message")
|
|
return
|
|
}
|
|
|
|
if message == nil {
|
|
logger.Warn().Msg("failed to marshal HID RPC message")
|
|
return
|
|
}
|
|
|
|
if err := session.HidChannel.Send(message); err != nil {
|
|
if errors.Is(err, io.ErrClosedPipe) {
|
|
logger.Debug().Err(err).Msg("HID RPC channel closed, skipping reportHidRPC")
|
|
return
|
|
}
|
|
logger.Warn().Err(err).Msg("failed to send HID RPC message")
|
|
}
|
|
}
|
|
|
|
func (s *Session) reportHidRPCKeyboardLedState(state usbgadget.KeyboardState) {
|
|
if !s.hidRPCAvailable {
|
|
writeJSONRPCEvent("keyboardLedState", state, s)
|
|
}
|
|
reportHidRPC(state, s)
|
|
}
|
|
|
|
func (s *Session) reportHidRPCKeysDownState(state usbgadget.KeysDownState) {
|
|
if !s.hidRPCAvailable {
|
|
usbLogger.Debug().Interface("state", state).Msg("reporting keys down state")
|
|
writeJSONRPCEvent("keysDownState", state, s)
|
|
}
|
|
usbLogger.Debug().Interface("state", state).Msg("reporting keys down state, calling reportHidRPC")
|
|
reportHidRPC(state, s)
|
|
}
|
|
|
|
func (s *Session) reportHidRPCKeyboardMacroState(state hidrpc.KeyboardMacroState) {
|
|
if !s.hidRPCAvailable {
|
|
writeJSONRPCEvent("keyboardMacroState", state, s)
|
|
}
|
|
reportHidRPC(state, s)
|
|
}
|