Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 1 addition & 2 deletions cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,10 +475,9 @@ func handleSessionRequest(
}

cloudLogger.Info().Interface("session", session).Msg("new session accepted")
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", int(getActiveSessions())))

if networkManager != nil && networkManager.IsUp() {
nativeInstance.UISetVar("main_screen", "home_screen")
Expand Down
177 changes: 99 additions & 78 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().Object("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().Object("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().Object("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, dropping 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 All @@ -244,10 +267,8 @@ func (s *Session) reportHidRPCKeyboardLedState(state usbgadget.KeyboardState) {

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)
}

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