Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,8 @@
],
"url": "./internal/ota/testdata/ota.schema.json"
}
]
],
"[go]": {
"editor.defaultFormatter": "golang.go"
}
}
2 changes: 1 addition & 1 deletion cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ func handleSessionRequest(
cloudLogger.Trace().Interface("session", session).Msg("new session accepted")

// Cancel any ongoing keyboard macro when session changes
cancelKeyboardMacro()
_ = cancelKeyboardMacro()

currentSession = session
_ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd})
Expand Down
2 changes: 1 addition & 1 deletion display.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func updateDisplay() {
nativeInstance.UpdateLabelIfChanged("hdmi_status_label", "Disconnected")
_, _ = nativeInstance.UIObjClearState("hdmi_status_label", "LV_STATE_CHECKED")
}
nativeInstance.UpdateLabelIfChanged("cloud_status_label", fmt.Sprintf("%d active", actionSessions))
nativeInstance.UpdateLabelIfChanged("cloud_status_label", fmt.Sprintf("%d active", getActiveSessions()))

if networkManager != nil && networkManager.IsUp() {
nativeInstance.UISetVar("main_screen", "home_screen")
Expand Down
175 changes: 99 additions & 76 deletions hidrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,98 +8,108 @@ import (

"github.com/jetkvm/kvm/internal/hidrpc"
"github.com/jetkvm/kvm/internal/usbgadget"
"github.com/jetkvm/kvm/internal/utils"
"github.com/rs/zerolog"
)

func handleHidRPCMessage(message hidrpc.Message, session *Session) {
var rpcErr error

func handleHidRPCMessage(message hidrpc.Message, session *Session) 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)
return handleHidRPCHandshake(session)
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)
return handleKeyboardMacro(message)
case hidrpc.TypeKeypressReport, hidrpc.TypeKeyboardReport:
return handleHidRPCKeyboardInput(message)
case hidrpc.TypeCancelKeyboardMacroReport:
rpcCancelKeyboardMacro()
return
return rpcCancelKeyboardMacro()
case hidrpc.TypeKeypressKeepAliveReport:
rpcErr = handleHidRPCKeypressKeepAlive(session)
return 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)
return handlePointerReport(message)
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")
return handleMouseReport(message)
}

if rpcErr != nil {
logger.Warn().Err(rpcErr).Msg("failed to handle HID RPC message")
return fmt.Errorf("unknown HID RPC message type %d", message.Type())
}

func handleHidRPCHandshake(session *Session) error {
hidRPCLogger.Debug().Msg("handling handshake")
message, err := hidrpc.NewHandshakeMessage().Marshal()
if err != nil {
return err
}
if err = session.HidChannel.Send(message); err != nil {
return err
}
session.hidRPCAvailable = true
return nil
}

func onHidMessage(msg hidQueueMessage, session *Session) {
func handleKeyboardMacro(message hidrpc.Message) error {
keyboardMacroReport, err := message.KeyboardMacroReport()
if err != nil {
return err
}
hidRPCLogger.Debug().Interface("keyboardMacroReport", keyboardMacroReport).Msg("handling keyboard macro")
return rpcExecuteKeyboardMacro(keyboardMacroReport.Steps)
}

func handleMouseReport(message hidrpc.Message) error {
mouseReport, err := message.MouseReport()
if err != nil {
return err
}
hidRPCLogger.Debug().Interface("mouseReport", mouseReport).Msg("handling relative mouse")
return rpcRelMouseReport(mouseReport.DX, mouseReport.DY, mouseReport.Button)
}

func handlePointerReport(message hidrpc.Message) error {
pointerReport, err := message.PointerReport()
if err != nil {
return err
}
hidRPCLogger.Debug().Interface("pointerReport", pointerReport).Msg("handling absolute pointer")
return rpcAbsMouseReport(pointerReport.X, pointerReport.Y, pointerReport.Button)
}

func onHidMessage(msg hidQueueMessage, session *Session, index int) {
logger := hidRPCLogger.With().Int("queueIndex", index).Str("channel", msg.channel).Logger()
data := msg.Data

scopedLogger := hidRPCLogger.With().
Str("channel", msg.channel).
Bytes("data", data).
Logger()
scopedLogger.Debug().Msg("HID RPC message received")
if logger.GetLevel() <= zerolog.TraceLevel {
logger.Trace().Object("data", utils.ByteSlice(data)).Msg("HID RPC message received")
}

if len(data) < 1 {
scopedLogger.Warn().Int("length", len(data)).Msg("received empty data in HID RPC message handler")
logger.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")
logger.Warn().Err(err).Msg("failed to unmarshal HID RPC message")
return
}

if scopedLogger.GetLevel() <= zerolog.DebugLevel {
scopedLogger = scopedLogger.With().Str("descr", message.String()).Logger()
if logger.GetLevel() <= zerolog.DebugLevel {
logger = logger.With().Str("descr", message.String()).Logger()
}

t := time.Now()

r := make(chan interface{})
go func() {
handleHidRPCMessage(message, session)
r <- nil
r <- handleHidRPCMessage(message, session)
}()
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")
logger.Warn().Msg("HID RPC message took too long")
case err := <-r:
logger.Debug().Dur("duration", time.Since(t)).Msg("HID RPC message handled")
if err != nil {
logger.Warn().Err(err.(error)).Msg("failed to handle HID RPC message")
}
}
}

Expand All @@ -108,12 +118,10 @@ func onHidMessage(msg hidQueueMessage, session *Session) {
// 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
const maxStaleness = 225 * time.Millisecond // discard ancient packets outright

func handleHidRPCKeypressKeepAlive(session *Session) error {
session.keepAliveJitterLock.Lock()
Expand All @@ -128,7 +136,6 @@ func handleHidRPCKeypressKeepAlive(session *Session) error {
return nil
}

validTick := true
timerExtension := baseExtension

if !session.lastKeepAliveArrivalTime.IsZero() {
Expand All @@ -147,14 +154,11 @@ func handleHidRPCKeypressKeepAlive(session *Session) error {
// 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
return nil
}
}
}

if !validTick {
return nil
}
// Only valid ticks update our state and extend the timer.
session.lastKeepAliveArrivalTime = now
session.lastTimerResetTime = now
Expand All @@ -167,27 +171,33 @@ func handleHidRPCKeypressKeepAlive(session *Session) error {
}

func handleHidRPCKeyboardInput(message hidrpc.Message) error {
logger := hidRPCLogger.With().Interface("message", message).Logger()

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
}
logger.Debug().Interface("keypressReport", keypressReport).Msg("handling key press")
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
}
logger.Debug().Interface("keyboardReport", keyboardReport).Msg("handling keyboard")
return rpcKeyboardReport(keyboardReport.Modifier, keyboardReport.Keys)
}

return fmt.Errorf("unknown HID RPC message type: %d", message.Type())
}

func reportHidRPC(params any, session *Session) {
logger := hidRPCLogger.With().Interface("params", params).Logger()

if session == nil {
logger.Warn().Msg("session is nil, skipping reportHidRPC")
return
Expand All @@ -205,6 +215,7 @@ func reportHidRPC(params any, session *Session) {
message []byte
err error
)

switch params := params.(type) {
case usbgadget.KeyboardState:
message, err = hidrpc.NewKeyboardLedMessage(params).Marshal()
Expand All @@ -216,23 +227,35 @@ func reportHidRPC(params any, session *Session) {
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
}
logger = logger.With().Type("type", params).Logger()

if message == nil {
logger.Warn().Msg("failed to marshal HID RPC message")
if err != nil || message == nil {
logger.Warn().Err(err).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
// fire and forget...
go func() {
t := time.Now()
r := make(chan interface{})
go func() {
logger.Trace().Msg("sending HID RPC report")
r <- session.HidChannel.Send(message)
}()
select {
case <-time.After(1 * time.Second):
logger.Warn().Msg("HID RPC report took too long")
case err := <-r:
logger.Debug().Dur("duration", time.Since(t)).Msg("HID RPC report sent")
if err != nil {
if errors.Is(err.(error), io.ErrClosedPipe) {
logger.Warn().Err(err.(error)).Msg("HID RPC channel closed, skipping reportHidRPC")
return
}
logger.Warn().Err(err.(error)).Msg("failed to send HID RPC report")
}
}
logger.Warn().Err(err).Msg("failed to send HID RPC message")
}
}()
}

func (s *Session) reportHidRPCKeyboardLedState(state usbgadget.KeyboardState) {
Expand Down
34 changes: 2 additions & 32 deletions internal/hidrpc/hidrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,9 @@ import (
"github.com/jetkvm/kvm/internal/usbgadget"
)

// MessageType is the type of the HID RPC message
type MessageType byte

const (
TypeHandshake MessageType = 0x01
TypeKeyboardReport MessageType = 0x02
TypePointerReport MessageType = 0x03
TypeWheelReport MessageType = 0x04
TypeKeypressReport MessageType = 0x05
TypeKeypressKeepAliveReport MessageType = 0x09
TypeMouseReport MessageType = 0x06
TypeKeyboardMacroReport MessageType = 0x07
TypeCancelKeyboardMacroReport MessageType = 0x08
TypeKeyboardLedState MessageType = 0x32
TypeKeydownState MessageType = 0x33
TypeKeyboardMacroState MessageType = 0x34
)

const (
Version byte = 0x01 // Version of the HID RPC protocol
Version byte = 0x01 // Version of the HID RPC protocol
MaximumQueues int = 4 // Maximum number of HID RPC queues
)

// GetQueueIndex returns the index of the queue to which the message should be enqueued.
Expand Down Expand Up @@ -57,19 +40,6 @@ func Unmarshal(data []byte, message *Message) error {
return nil
}

// Marshal marshals the HID RPC message to the data.
func Marshal(message *Message) ([]byte, error) {
if message.t == 0 {
return nil, fmt.Errorf("invalid message type: %d", message.t)
}

data := make([]byte, len(message.d)+1)
data[0] = byte(message.t)
copy(data[1:], message.d)

return data, nil
}

// NewHandshakeMessage creates a new handshake message.
func NewHandshakeMessage() *Message {
return &Message{
Expand Down
Loading