This commit is contained in:
Marc Brooks 2025-10-01 21:44:32 +02:00 committed by GitHub
commit f9125e8639
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 372 additions and 112 deletions

View File

@ -478,7 +478,7 @@ func handleSessionRequest(
cloudLogger.Trace().Interface("session", session).Msg("new session accepted") cloudLogger.Trace().Interface("session", session).Msg("new session accepted")
// Cancel any ongoing keyboard macro when session changes // Cancel any ongoing keyboard macro when session changes
cancelKeyboardMacro() cancelAllRunningKeyboardMacros()
currentSession = session currentSession = session
_ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd}) _ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd})

View File

@ -26,20 +26,45 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
return return
} }
session.hidRPCAvailable = true session.hidRPCAvailable = true
case hidrpc.TypeKeypressReport, hidrpc.TypeKeyboardReport: case hidrpc.TypeKeypressReport, hidrpc.TypeKeyboardReport:
rpcErr = handleHidRPCKeyboardInput(message) rpcErr = handleHidRPCKeyboardInput(message)
case hidrpc.TypeKeyboardMacroReport: case hidrpc.TypeKeyboardMacroReport:
keyboardMacroReport, err := message.KeyboardMacroReport() keyboardMacroReport, err := message.KeyboardMacroReport()
if err != nil { if err != nil {
logger.Warn().Err(err).Msg("failed to get keyboard macro report") logger.Warn().Err(err).Msg("failed to get keyboard macro report")
return return
} }
rpcErr = rpcExecuteKeyboardMacro(keyboardMacroReport.Steps) token := rpcExecuteKeyboardMacro(keyboardMacroReport.IsPaste, keyboardMacroReport.Steps)
logger.Debug().Str("token", token.String()).Msg("started keyboard macro")
message, err := hidrpc.NewKeyboardMacroTokenMessage(token).Marshal()
if err != nil {
logger.Warn().Err(err).Msg("failed to marshal running macro token message")
return
}
if err := session.HidChannel.Send(message); err != nil {
logger.Warn().Err(err).Msg("failed to send running macro token message")
return
}
case hidrpc.TypeCancelKeyboardMacroReport: case hidrpc.TypeCancelKeyboardMacroReport:
rpcCancelKeyboardMacro() rpcCancelKeyboardMacro()
return return
case hidrpc.TypeKeyboardMacroTokenState:
tokenState, err := message.KeyboardMacroTokenState()
if err != nil {
logger.Warn().Err(err).Msg("failed to get keyboard macro token")
return
}
rpcCancelKeyboardMacroByToken(tokenState.Token)
return
case hidrpc.TypeKeypressKeepAliveReport: case hidrpc.TypeKeypressKeepAliveReport:
rpcErr = handleHidRPCKeypressKeepAlive(session) rpcErr = handleHidRPCKeypressKeepAlive(session)
case hidrpc.TypePointerReport: case hidrpc.TypePointerReport:
pointerReport, err := message.PointerReport() pointerReport, err := message.PointerReport()
if err != nil { if err != nil {
@ -47,6 +72,7 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
return return
} }
rpcErr = rpcAbsMouseReport(pointerReport.X, pointerReport.Y, pointerReport.Button) rpcErr = rpcAbsMouseReport(pointerReport.X, pointerReport.Y, pointerReport.Button)
case hidrpc.TypeMouseReport: case hidrpc.TypeMouseReport:
mouseReport, err := message.MouseReport() mouseReport, err := message.MouseReport()
if err != nil { if err != nil {
@ -54,6 +80,7 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
return return
} }
rpcErr = rpcRelMouseReport(mouseReport.DX, mouseReport.DY, mouseReport.Button) rpcErr = rpcRelMouseReport(mouseReport.DX, mouseReport.DY, mouseReport.Button)
default: default:
logger.Warn().Uint8("type", uint8(message.Type())).Msg("unknown HID RPC message type") logger.Warn().Uint8("type", uint8(message.Type())).Msg("unknown HID RPC message type")
} }
@ -65,15 +92,18 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
func onHidMessage(msg hidQueueMessage, session *Session) { func onHidMessage(msg hidQueueMessage, session *Session) {
data := msg.Data data := msg.Data
dataLen := len(data)
scopedLogger := hidRPCLogger.With(). scopedLogger := hidRPCLogger.With().
Str("channel", msg.channel). Str("channel", msg.channel).
Bytes("data", data). Dur("timelimit", msg.timelimit).
Int("data_len", dataLen).
Bytes("data", data[:min(dataLen, 32)]).
Logger() Logger()
scopedLogger.Debug().Msg("HID RPC message received") scopedLogger.Debug().Msg("HID RPC message received")
if len(data) < 1 { if dataLen < 1 {
scopedLogger.Warn().Int("length", len(data)).Msg("received empty data in HID RPC message handler") scopedLogger.Warn().Msg("received empty data in HID RPC message handler")
return return
} }
@ -96,7 +126,7 @@ func onHidMessage(msg hidQueueMessage, session *Session) {
r <- nil r <- nil
}() }()
select { select {
case <-time.After(1 * time.Second): case <-time.After(msg.timelimit * time.Second):
scopedLogger.Warn().Msg("HID RPC message timed out") scopedLogger.Warn().Msg("HID RPC message timed out")
case <-r: case <-r:
scopedLogger.Debug().Dur("duration", time.Since(t)).Msg("HID RPC message handled") scopedLogger.Debug().Dur("duration", time.Since(t)).Msg("HID RPC message handled")
@ -212,6 +242,8 @@ func reportHidRPC(params any, session *Session) {
message, err = hidrpc.NewKeydownStateMessage(params).Marshal() message, err = hidrpc.NewKeydownStateMessage(params).Marshal()
case hidrpc.KeyboardMacroState: case hidrpc.KeyboardMacroState:
message, err = hidrpc.NewKeyboardMacroStateMessage(params.State, params.IsPaste).Marshal() message, err = hidrpc.NewKeyboardMacroStateMessage(params.State, params.IsPaste).Marshal()
case hidrpc.KeyboardMacroTokenState:
message, err = hidrpc.NewKeyboardMacroTokenMessage(params.Token).Marshal()
default: default:
err = fmt.Errorf("unknown HID RPC message type: %T", params) err = fmt.Errorf("unknown HID RPC message type: %T", params)
} }

View File

@ -2,7 +2,9 @@ package hidrpc
import ( import (
"fmt" "fmt"
"time"
"github.com/google/uuid"
"github.com/jetkvm/kvm/internal/usbgadget" "github.com/jetkvm/kvm/internal/usbgadget"
) )
@ -22,26 +24,34 @@ const (
TypeKeyboardLedState MessageType = 0x32 TypeKeyboardLedState MessageType = 0x32
TypeKeydownState MessageType = 0x33 TypeKeydownState MessageType = 0x33
TypeKeyboardMacroState MessageType = 0x34 TypeKeyboardMacroState MessageType = 0x34
TypeKeyboardMacroTokenState MessageType = 0x35
) )
type QueueIndex int
const ( const (
Version byte = 0x01 // Version of the HID RPC protocol Version byte = 0x01 // Version of the HID RPC protocol
HandshakeQueue int = 0 // Queue index for handshake messages
KeyboardQueue int = 1 // Queue index for keyboard messages
MouseQueue int = 2 // Queue index for mouse messages
MacroQueue int = 3 // Queue index for macro messages
OtherQueue int = 4 // Queue index for other messages
) )
// GetQueueIndex returns the index of the queue to which the message should be enqueued. // GetQueueIndex returns the index of the queue to which the message should be enqueued.
func GetQueueIndex(messageType MessageType) int { func GetQueueIndex(messageType MessageType) (int, time.Duration) {
switch messageType { switch messageType {
case TypeHandshake: case TypeHandshake:
return 0 return HandshakeQueue, 1
case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardMacroReport, TypeKeyboardLedState, TypeKeydownState, TypeKeyboardMacroState: case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardLedState, TypeKeydownState, TypeKeyboardMacroState:
return 1 return KeyboardQueue, 1
case TypePointerReport, TypeMouseReport, TypeWheelReport: case TypePointerReport, TypeMouseReport, TypeWheelReport:
return 2 return MouseQueue, 1
// we don't want to block the queue for this message // we don't want to block the queue for these messages
case TypeCancelKeyboardMacroReport: case TypeKeyboardMacroReport, TypeCancelKeyboardMacroReport, TypeKeyboardMacroTokenState:
return 3 return MacroQueue, 60 // 1 minute timeout
default: default:
return 3 return OtherQueue, 5
} }
} }
@ -121,3 +131,13 @@ func NewKeyboardMacroStateMessage(state bool, isPaste bool) *Message {
d: data, d: data,
} }
} }
// NewKeyboardMacroTokenMessage creates a new keyboard macro token message.
func NewKeyboardMacroTokenMessage(token uuid.UUID) *Message {
data, _ := token.MarshalBinary()
return &Message{
t: TypeKeyboardMacroState,
d: data,
}
}

View File

@ -3,6 +3,8 @@ package hidrpc
import ( import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"github.com/google/uuid"
) )
// Message .. // Message ..
@ -23,6 +25,9 @@ func (m *Message) Type() MessageType {
func (m *Message) String() string { func (m *Message) String() string {
switch m.t { switch m.t {
case TypeHandshake: case TypeHandshake:
if len(m.d) != 0 {
return fmt.Sprintf("Handshake{Malformed: %v}", m.d)
}
return "Handshake" return "Handshake"
case TypeKeypressReport: case TypeKeypressReport:
if len(m.d) < 2 { if len(m.d) < 2 {
@ -45,12 +50,45 @@ func (m *Message) String() string {
} }
return fmt.Sprintf("MouseReport{DX: %d, DY: %d, Button: %d}", m.d[0], m.d[1], m.d[2]) return fmt.Sprintf("MouseReport{DX: %d, DY: %d, Button: %d}", m.d[0], m.d[1], m.d[2])
case TypeKeypressKeepAliveReport: case TypeKeypressKeepAliveReport:
if len(m.d) != 0 {
return fmt.Sprintf("KeypressKeepAliveReport{Malformed: %v}", m.d)
}
return "KeypressKeepAliveReport" return "KeypressKeepAliveReport"
case TypeWheelReport:
if len(m.d) < 3 {
return fmt.Sprintf("WheelReport{Malformed: %v}", m.d)
}
return fmt.Sprintf("WheelReport{Vertical: %d, Horizontal: %d}", int8(m.d[0]), int8(m.d[1]))
case TypeKeyboardMacroReport: case TypeKeyboardMacroReport:
if len(m.d) < 5 { if len(m.d) < 5 {
return fmt.Sprintf("KeyboardMacroReport{Malformed: %v}", m.d) return fmt.Sprintf("KeyboardMacroReport{Malformed: %v}", m.d)
} }
return fmt.Sprintf("KeyboardMacroReport{IsPaste: %v, Length: %d}", m.d[0] == uint8(1), binary.BigEndian.Uint32(m.d[1:5])) return fmt.Sprintf("KeyboardMacroReport{IsPaste: %v, Length: %d}", m.d[0] == uint8(1), binary.BigEndian.Uint32(m.d[1:5]))
case TypeCancelKeyboardMacroReport:
if len(m.d) != 0 {
return fmt.Sprintf("CancelKeyboardMacroReport{Malformed: %v}", m.d)
}
return "CancelKeyboardMacroReport"
case TypeKeyboardMacroTokenState:
if len(m.d) != 16 {
return fmt.Sprintf("KeyboardMacroTokenState{Malformed: %v}", m.d)
}
return fmt.Sprintf("KeyboardMacroTokenState{Token: %s}", uuid.Must(uuid.FromBytes(m.d)).String())
case TypeKeyboardLedState:
if len(m.d) < 1 {
return fmt.Sprintf("KeyboardLedState{Malformed: %v}", m.d)
}
return fmt.Sprintf("KeyboardLedState{State: %d}", m.d[0])
case TypeKeydownState:
if len(m.d) < 1 {
return fmt.Sprintf("KeydownState{Malformed: %v}", m.d)
}
return fmt.Sprintf("KeydownState{State: %d}", m.d[0])
case TypeKeyboardMacroState:
if len(m.d) < 2 {
return fmt.Sprintf("KeyboardMacroState{Malformed: %v}", m.d)
}
return fmt.Sprintf("KeyboardMacroState{State: %v, IsPaste: %v}", m.d[0] == uint8(1), m.d[1] == uint8(1))
default: default:
return fmt.Sprintf("Unknown{Type: %d, Data: %v}", m.t, m.d) return fmt.Sprintf("Unknown{Type: %d, Data: %v}", m.t, m.d)
} }
@ -67,7 +105,9 @@ func (m *Message) KeypressReport() (KeypressReport, error) {
if m.t != TypeKeypressReport { if m.t != TypeKeypressReport {
return KeypressReport{}, fmt.Errorf("invalid message type: %d", m.t) return KeypressReport{}, fmt.Errorf("invalid message type: %d", m.t)
} }
if len(m.d) < 2 {
return KeypressReport{}, fmt.Errorf("invalid message data length: %d", len(m.d))
}
return KeypressReport{ return KeypressReport{
Key: m.d[0], Key: m.d[0],
Press: m.d[1] == uint8(1), Press: m.d[1] == uint8(1),
@ -95,7 +135,7 @@ func (m *Message) KeyboardReport() (KeyboardReport, error) {
// Macro .. // Macro ..
type KeyboardMacroStep struct { type KeyboardMacroStep struct {
Modifier byte // 1 byte Modifier byte // 1 byte
Keys []byte // 6 bytes: hidKeyBufferSize Keys []byte // 6 bytes: HidKeyBufferSize
Delay uint16 // 2 bytes Delay uint16 // 2 bytes
} }
type KeyboardMacroReport struct { type KeyboardMacroReport struct {
@ -105,7 +145,7 @@ type KeyboardMacroReport struct {
} }
// HidKeyBufferSize is the size of the keys buffer in the keyboard report. // HidKeyBufferSize is the size of the keys buffer in the keyboard report.
const HidKeyBufferSize = 6 const HidKeyBufferSize int = 6
// KeyboardMacroReport returns the keyboard macro report from the message. // KeyboardMacroReport returns the keyboard macro report from the message.
func (m *Message) KeyboardMacroReport() (KeyboardMacroReport, error) { func (m *Message) KeyboardMacroReport() (KeyboardMacroReport, error) {
@ -205,3 +245,29 @@ func (m *Message) KeyboardMacroState() (KeyboardMacroState, error) {
IsPaste: m.d[1] == uint8(1), IsPaste: m.d[1] == uint8(1),
}, nil }, nil
} }
type KeyboardMacroTokenState struct {
Token uuid.UUID
}
// KeyboardMacroTokenState returns the keyboard macro token UUID from the message.
func (m *Message) KeyboardMacroTokenState() (KeyboardMacroTokenState, error) {
if m.t != TypeKeyboardMacroTokenState {
return KeyboardMacroTokenState{}, fmt.Errorf("invalid message type: %d", m.t)
}
if len(m.d) == 0 {
return KeyboardMacroTokenState{Token: uuid.Nil}, nil
}
if len(m.d) != 16 {
return KeyboardMacroTokenState{}, fmt.Errorf("invalid UUID length: %d", len(m.d))
}
token, err := uuid.FromBytes(m.d)
if err != nil {
return KeyboardMacroTokenState{}, fmt.Errorf("invalid UUID: %v", err)
}
return KeyboardMacroTokenState{Token: token}, nil
}

View File

@ -31,6 +31,8 @@ var keyboardReportDesc = []byte{
0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */
0x09, 0x06, /* USAGE (Keyboard) */ 0x09, 0x06, /* USAGE (Keyboard) */
0xa1, 0x01, /* COLLECTION (Application) */ 0xa1, 0x01, /* COLLECTION (Application) */
/* 8 modifier bits */
0x05, 0x07, /* USAGE_PAGE (Keyboard) */ 0x05, 0x07, /* USAGE_PAGE (Keyboard) */
0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */ 0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */
0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */ 0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */
@ -39,27 +41,47 @@ var keyboardReportDesc = []byte{
0x75, 0x01, /* REPORT_SIZE (1) */ 0x75, 0x01, /* REPORT_SIZE (1) */
0x95, 0x08, /* REPORT_COUNT (8) */ 0x95, 0x08, /* REPORT_COUNT (8) */
0x81, 0x02, /* INPUT (Data,Var,Abs) */ 0x81, 0x02, /* INPUT (Data,Var,Abs) */
/* 8 bits of padding */
0x95, 0x01, /* REPORT_COUNT (1) */ 0x95, 0x01, /* REPORT_COUNT (1) */
0x75, 0x08, /* REPORT_SIZE (8) */ 0x75, 0x08, /* REPORT_SIZE (8) */
0x81, 0x03, /* INPUT (Cnst,Var,Abs) */ 0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
/* 6 key codes for the 104 key keyboard */
0x95, 0x06, /* REPORT_COUNT (6) */
0x75, 0x08, /* REPORT_SIZE (8) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0xE7, /* LOGICAL_MAXIMUM (104-key HID) */
0x05, 0x07, /* USAGE_PAGE (Keyboard) */
0x19, 0x00, /* USAGE_MINIMUM (Reserved) */
0x29, 0xE7, /* USAGE_MAXIMUM (Keyboard Right GUI) */
0x81, 0x00, /* INPUT (Data,Ary,Abs) */
/* LED report 5 bits for Num Lock through Kana */
0x95, 0x05, /* REPORT_COUNT (5) */ 0x95, 0x05, /* REPORT_COUNT (5) */
0x75, 0x01, /* REPORT_SIZE (1) */ 0x75, 0x01, /* REPORT_SIZE (1) */
0x05, 0x08, /* USAGE_PAGE (LEDs) */ 0x05, 0x08, /* USAGE_PAGE (LEDs) */
0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */ 0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */
0x29, 0x05, /* USAGE_MAXIMUM (Kana) */ 0x29, 0x05, /* USAGE_MAXIMUM (Kana) */
0x91, 0x02, /* OUTPUT (Data,Var,Abs) */ 0x91, 0x02, /* OUTPUT (Data,Var,Abs) */
/* 1 bit of padding for the Power LED (ignored) */
0x95, 0x01, /* REPORT_COUNT (1) */ 0x95, 0x01, /* REPORT_COUNT (1) */
0x75, 0x03, /* REPORT_SIZE (3) */ 0x75, 0x03, /* REPORT_SIZE (1) */
0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */
/* LED report 1 bit for Shift */
0x95, 0x01, /* REPORT_COUNT (1) */
0x75, 0x01, /* REPORT_SIZE (1) */
0x05, 0x08, /* USAGE_PAGE (LEDs) */
0x19, 0x07, /* USAGE_MINIMUM (Shift) */
0x29, 0x07, /* USAGE_MAXIMUM (Shift) */
0x91, 0x02, /* OUTPUT (Data,Var,Abs) */
/* 1 bit of padding for the rest of the byte */
0x95, 0x01, /* REPORT_COUNT (1) */
0x75, 0x03, /* REPORT_SIZE (1) */
0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */ 0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */
0x95, 0x06, /* REPORT_COUNT (6) */
0x75, 0x08, /* REPORT_SIZE (8) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x65, /* LOGICAL_MAXIMUM (101) */
0x05, 0x07, /* USAGE_PAGE (Keyboard) */
0x19, 0x00, /* USAGE_MINIMUM (Reserved) */
0x29, 0x65, /* USAGE_MAXIMUM (Keyboard Application) */
0x81, 0x00, /* INPUT (Data,Ary,Abs) */
0xc0, /* END_COLLECTION */ 0xc0, /* END_COLLECTION */
} }
@ -153,6 +175,16 @@ func (u *UsbGadget) SetOnKeysDownChange(f func(state KeysDownState)) {
u.onKeysDownChange = &f u.onKeysDownChange = &f
} }
var suspendedKeyDownMessages bool = false
func (u *UsbGadget) SuspendKeyDownMessages() {
suspendedKeyDownMessages = true
}
func (u *UsbGadget) ResumeSuspendKeyDownMessages() {
suspendedKeyDownMessages = false
}
func (u *UsbGadget) SetOnKeepAliveReset(f func()) { func (u *UsbGadget) SetOnKeepAliveReset(f func()) {
u.onKeepAliveReset = &f u.onKeepAliveReset = &f
} }
@ -169,9 +201,9 @@ func (u *UsbGadget) scheduleAutoRelease(key byte) {
} }
// TODO: make this configurable // TODO: make this configurable
// We currently hardcode the duration to 100ms // We currently hardcode the duration to the default of 100ms
// However, it should be the same as the duration of the keep-alive reset called baseExtension. // However, it should be the same as the duration of the keep-alive reset called baseExtension.
u.kbdAutoReleaseTimers[key] = time.AfterFunc(100*time.Millisecond, func() { u.kbdAutoReleaseTimers[key] = time.AfterFunc(DefaultAutoReleaseDuration, func() {
u.performAutoRelease(key) u.performAutoRelease(key)
}) })
} }
@ -314,6 +346,7 @@ var keyboardWriteHidFileLock sync.Mutex
func (u *UsbGadget) keyboardWriteHidFile(modifier byte, keys []byte) error { func (u *UsbGadget) keyboardWriteHidFile(modifier byte, keys []byte) error {
keyboardWriteHidFileLock.Lock() keyboardWriteHidFileLock.Lock()
defer keyboardWriteHidFileLock.Unlock() defer keyboardWriteHidFileLock.Unlock()
if err := u.openKeyboardHidFile(); err != nil { if err := u.openKeyboardHidFile(); err != nil {
return err return err
} }
@ -353,7 +386,7 @@ func (u *UsbGadget) UpdateKeysDown(modifier byte, keys []byte) KeysDownState {
u.keysDownState = state u.keysDownState = state
u.keyboardStateLock.Unlock() u.keyboardStateLock.Unlock()
if u.onKeysDownChange != nil { if u.onKeysDownChange != nil && !suspendedKeyDownMessages {
(*u.onKeysDownChange)(state) // this enques to the outgoing hidrpc queue via usb.go → currentSession.enqueueKeysDownState(...) (*u.onKeysDownChange)(state) // this enques to the outgoing hidrpc queue via usb.go → currentSession.enqueueKeysDownState(...)
} }
return state return state
@ -484,6 +517,10 @@ func (u *UsbGadget) keypressReport(key byte, press bool) (KeysDownState, error)
} }
err := u.keyboardWriteHidFile(modifier, keys) err := u.keyboardWriteHidFile(modifier, keys)
if err != nil {
u.log.Warn().Uint8("modifier", modifier).Uints8("keys", keys).Msg("Could not write keyboard report to hidg0")
}
return u.UpdateKeysDown(modifier, keys), err return u.UpdateKeysDown(modifier, keys), err
} }

View File

@ -1,7 +1,6 @@
package kvm package kvm
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
@ -14,6 +13,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/google/uuid"
"github.com/pion/webrtc/v4" "github.com/pion/webrtc/v4"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.bug.st/serial" "go.bug.st/serial"
@ -1084,91 +1084,154 @@ func rpcSetLocalLoopbackOnly(enabled bool) error {
return nil return nil
} }
type RunningMacro struct {
cancel context.CancelFunc
isPaste bool
}
var ( var (
keyboardMacroCancel context.CancelFunc keyboardMacroCancelMap map[uuid.UUID]RunningMacro
keyboardMacroLock sync.Mutex keyboardMacroLock sync.Mutex
keyboardMacroOnce sync.Once
) )
// cancelKeyboardMacro cancels any ongoing keyboard macro execution func getKeyboardMacroCancelMap() map[uuid.UUID]RunningMacro {
func cancelKeyboardMacro() { keyboardMacroOnce.Do(func() {
keyboardMacroCancelMap = make(map[uuid.UUID]RunningMacro)
})
return keyboardMacroCancelMap
}
func addKeyboardMacro(isPaste bool, cancel context.CancelFunc) uuid.UUID {
keyboardMacroLock.Lock() keyboardMacroLock.Lock()
defer keyboardMacroLock.Unlock() defer keyboardMacroLock.Unlock()
cancelMap := getKeyboardMacroCancelMap()
if keyboardMacroCancel != nil { token := uuid.New() // Generate a unique token
keyboardMacroCancel() cancelMap[token] = RunningMacro{
logger.Info().Msg("canceled keyboard macro") isPaste: isPaste,
keyboardMacroCancel = nil cancel: cancel,
}
return token
}
func removeRunningKeyboardMacro(token uuid.UUID) {
keyboardMacroLock.Lock()
defer keyboardMacroLock.Unlock()
cancelMap := getKeyboardMacroCancelMap()
delete(cancelMap, token)
}
func cancelRunningKeyboardMacro(token uuid.UUID) {
keyboardMacroLock.Lock()
defer keyboardMacroLock.Unlock()
cancelMap := getKeyboardMacroCancelMap()
if runningMacro, exists := cancelMap[token]; exists {
runningMacro.cancel()
delete(cancelMap, token)
logger.Info().Interface("token", token).Msg("canceled keyboard macro by token")
} else {
logger.Debug().Interface("token", token).Msg("no running keyboard macro found for token")
} }
} }
func setKeyboardMacroCancel(cancel context.CancelFunc) { func cancelAllRunningKeyboardMacros() {
keyboardMacroLock.Lock() keyboardMacroLock.Lock()
defer keyboardMacroLock.Unlock() defer keyboardMacroLock.Unlock()
cancelMap := getKeyboardMacroCancelMap()
keyboardMacroCancel = cancel for token, runningMacro := range cancelMap {
runningMacro.cancel()
delete(cancelMap, token)
logger.Info().Interface("token", token).Msg("cancelled keyboard macro")
}
} }
func rpcExecuteKeyboardMacro(macro []hidrpc.KeyboardMacroStep) error { func reportRunningMacrosState() {
cancelKeyboardMacro() if currentSession != nil {
keyboardMacroLock.Lock()
defer keyboardMacroLock.Unlock()
cancelMap := getKeyboardMacroCancelMap()
isPaste := false
for _, runningMacro := range cancelMap {
if runningMacro.isPaste {
isPaste = true
break
}
}
state := hidrpc.KeyboardMacroState{
State: len(cancelMap) > 0,
IsPaste: isPaste,
}
currentSession.reportHidRPCKeyboardMacroState(state)
}
}
func rpcExecuteKeyboardMacro(isPaste bool, macro []hidrpc.KeyboardMacroStep) uuid.UUID {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
setKeyboardMacroCancel(cancel) token := addKeyboardMacro(isPaste, cancel)
reportRunningMacrosState()
s := hidrpc.KeyboardMacroState{ go func() {
State: true, defer reportRunningMacrosState() // this executes last, so the map is already updated
IsPaste: true, defer removeRunningKeyboardMacro(token) // this executes first, to update the map
}
if currentSession != nil { err := executeKeyboardMacro(ctx, isPaste, macro)
currentSession.reportHidRPCKeyboardMacroState(s) if err != nil {
} logger.Error().Err(err).Interface("token", token).Bool("isPaste", isPaste).Msg("keyboard macro execution failed")
}
}()
err := rpcDoExecuteKeyboardMacro(ctx, macro) return token
setKeyboardMacroCancel(nil)
s.State = false
if currentSession != nil {
currentSession.reportHidRPCKeyboardMacroState(s)
}
return err
} }
func rpcCancelKeyboardMacro() { func rpcCancelKeyboardMacro() {
cancelKeyboardMacro() defer reportRunningMacrosState()
cancelAllRunningKeyboardMacros()
} }
var keyboardClearStateKeys = make([]byte, hidrpc.HidKeyBufferSize) func rpcCancelKeyboardMacroByToken(token uuid.UUID) {
defer reportRunningMacrosState()
func isClearKeyStep(step hidrpc.KeyboardMacroStep) bool { if token == uuid.Nil {
return step.Modifier == 0 && bytes.Equal(step.Keys, keyboardClearStateKeys) cancelAllRunningKeyboardMacros()
} else {
cancelRunningKeyboardMacro(token)
}
} }
func rpcDoExecuteKeyboardMacro(ctx context.Context, macro []hidrpc.KeyboardMacroStep) error { func executeKeyboardMacro(ctx context.Context, isPaste bool, macro []hidrpc.KeyboardMacroStep) error {
logger.Debug().Interface("macro", macro).Msg("Executing keyboard macro") logger.Debug().
Int("macro_steps", len(macro)).
Bool("isPaste", isPaste).
Msg("Executing keyboard macro")
// don't report keyboard state changes while executing the macro
gadget.SuspendKeyDownMessages()
defer gadget.ResumeSuspendKeyDownMessages()
for i, step := range macro { for i, step := range macro {
delay := time.Duration(step.Delay) * time.Millisecond delay := time.Duration(step.Delay) * time.Millisecond
err := rpcKeyboardReport(step.Modifier, step.Keys) err := rpcKeyboardReport(step.Modifier, step.Keys)
if err != nil { if err != nil {
logger.Warn().Err(err).Msg("failed to execute keyboard macro") logger.Warn().Err(err).Int("step", i).Msg("failed to execute keyboard macro")
return err return err
} }
// notify the device that the keyboard state is being cleared
if isClearKeyStep(step) {
gadget.UpdateKeysDown(0, keyboardClearStateKeys)
}
// Use context-aware sleep that can be cancelled // Use context-aware sleep that can be cancelled
select { select {
case <-time.After(delay): case <-time.After(delay):
// Sleep completed normally // Sleep completed normally
case <-ctx.Done(): case <-ctx.Done():
// make sure keyboard state is reset // make sure keyboard state is reset and the client gets notified
err := rpcKeyboardReport(0, keyboardClearStateKeys) gadget.ResumeSuspendKeyDownMessages()
err := rpcKeyboardReport(0, make([]byte, hidrpc.HidKeyBufferSize))
if err != nil { if err != nil {
logger.Warn().Err(err).Msg("failed to reset keyboard state") logger.Warn().Err(err).Msg("failed to reset keyboard state")
} }

14
ui/package-lock.json generated
View File

@ -37,6 +37,7 @@
"recharts": "^3.2.1", "recharts": "^3.2.1",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"usehooks-ts": "^3.1.1", "usehooks-ts": "^3.1.1",
"uuid": "^13.0.0",
"validator": "^13.15.15", "validator": "^13.15.15",
"zustand": "^4.5.2" "zustand": "^4.5.2"
}, },
@ -6890,6 +6891,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/uuid": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/validator": { "node_modules/validator": {
"version": "13.15.15", "version": "13.15.15",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",

View File

@ -48,6 +48,7 @@
"recharts": "^3.2.1", "recharts": "^3.2.1",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"usehooks-ts": "^3.1.1", "usehooks-ts": "^3.1.1",
"uuid": "^13.0.0",
"validator": "^13.15.15", "validator": "^13.15.15",
"zustand": "^4.5.2" "zustand": "^4.5.2"
}, },

View File

@ -188,18 +188,18 @@ export default function PasteModal() {
type="number" type="number"
label="Delay between keys" label="Delay between keys"
placeholder="Delay between keys" placeholder="Delay between keys"
min={50} min={0}
max={65534} max={65534}
value={delayValue} value={delayValue}
onChange={e => { onChange={e => {
setDelayValue(parseInt(e.target.value, 10)); setDelayValue(parseInt(e.target.value, 10));
}} }}
/> />
{delayValue < 50 || delayValue > 65534 && ( {delayValue < defaultDelay || delayValue > 65534 && (
<div className="mt-2 flex items-center gap-x-2"> <div className="mt-2 flex items-center gap-x-2">
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" /> <ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
<span className="text-xs text-red-500 dark:text-red-400"> <span className="text-xs text-red-500 dark:text-red-400">
Delay must be between 50 and 65534 Delay should be between 20 and 65534
</span> </span>
</div> </div>
)} )}

View File

@ -1,3 +1,5 @@
import { parse as uuidParse , stringify as uuidStringify } from "uuid";
import { hidKeyBufferSize, KeyboardLedState, KeysDownState } from "./stores"; import { hidKeyBufferSize, KeyboardLedState, KeysDownState } from "./stores";
export const HID_RPC_MESSAGE_TYPES = { export const HID_RPC_MESSAGE_TYPES = {
@ -13,6 +15,7 @@ export const HID_RPC_MESSAGE_TYPES = {
KeyboardLedState: 0x32, KeyboardLedState: 0x32,
KeysDownState: 0x33, KeysDownState: 0x33,
KeyboardMacroState: 0x34, KeyboardMacroState: 0x34,
CancelKeyboardMacroByTokenReport: 0x35,
} }
export type HidRpcMessageType = typeof HID_RPC_MESSAGE_TYPES[keyof typeof HID_RPC_MESSAGE_TYPES]; export type HidRpcMessageType = typeof HID_RPC_MESSAGE_TYPES[keyof typeof HID_RPC_MESSAGE_TYPES];
@ -299,7 +302,7 @@ export class KeyboardMacroStateMessage extends RpcMessage {
} }
public static unmarshal(data: Uint8Array): KeyboardMacroStateMessage | undefined { public static unmarshal(data: Uint8Array): KeyboardMacroStateMessage | undefined {
if (data.length < 1) { if (data.length < 2) {
throw new Error(`Invalid keyboard macro state report message length: ${data.length}`); throw new Error(`Invalid keyboard macro state report message length: ${data.length}`);
} }
@ -378,13 +381,30 @@ export class PointerReportMessage extends RpcMessage {
} }
export class CancelKeyboardMacroReportMessage extends RpcMessage { export class CancelKeyboardMacroReportMessage extends RpcMessage {
token: string;
constructor() { constructor(token: string) {
super(HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroReport); super(HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroReport);
this.token = (token == null || token === undefined || token === "")
? "00000000-0000-0000-0000-000000000000"
: token;
} }
marshal(): Uint8Array { marshal(): Uint8Array {
return new Uint8Array([this.messageType]); const tokenBytes = uuidParse(this.token);
return new Uint8Array([this.messageType, ...tokenBytes]);
}
public static unmarshal(data: Uint8Array): CancelKeyboardMacroReportMessage | undefined {
if (data.length == 0) {
return new CancelKeyboardMacroReportMessage("00000000-0000-0000-0000-000000000000");
}
if (data.length != 16) {
throw new Error(`Invalid cancel message length: ${data.length}`);
}
return new CancelKeyboardMacroReportMessage(uuidStringify(data.slice(0, 16)));
} }
} }
@ -430,6 +450,7 @@ export const messageRegistry = {
[HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroReport]: CancelKeyboardMacroReportMessage, [HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroReport]: CancelKeyboardMacroReportMessage,
[HID_RPC_MESSAGE_TYPES.KeyboardMacroState]: KeyboardMacroStateMessage, [HID_RPC_MESSAGE_TYPES.KeyboardMacroState]: KeyboardMacroStateMessage,
[HID_RPC_MESSAGE_TYPES.KeypressKeepAliveReport]: KeypressKeepAliveMessage, [HID_RPC_MESSAGE_TYPES.KeypressKeepAliveReport]: KeypressKeepAliveMessage,
[HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroByTokenReport]: CancelKeyboardMacroReportMessage,
} }
export const unmarshalHidRpcMessage = (data: Uint8Array): RpcMessage | undefined => { export const unmarshalHidRpcMessage = (data: Uint8Array): RpcMessage | undefined => {

View File

@ -142,7 +142,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
const cancelOngoingKeyboardMacro = useCallback( const cancelOngoingKeyboardMacro = useCallback(
() => { () => {
sendMessage(new CancelKeyboardMacroReportMessage()); sendMessage(new CancelKeyboardMacroReportMessage(""));
}, },
[sendMessage], [sendMessage],
); );

View File

@ -277,7 +277,6 @@ export default function useKeyboard() {
cancelKeepAlive(); cancelKeepAlive();
}, [cancelKeepAlive]); }, [cancelKeepAlive]);
// executeMacro is used to execute a macro consisting of multiple steps. // executeMacro is used to execute a macro consisting of multiple steps.
// Each step can have multiple keys, multiple modifiers and a delay. // Each step can have multiple keys, multiple modifiers and a delay.
// The keys and modifiers are pressed together and held for the delay duration. // The keys and modifiers are pressed together and held for the delay duration.
@ -292,9 +291,7 @@ export default function useKeyboard() {
for (const [_, step] of steps.entries()) { for (const [_, step] of steps.entries()) {
const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean); const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean);
const modifierMask: number = (step.modifiers || []) const modifierMask: number = (step.modifiers || [])
.map(mod => modifiers[mod]) .map(mod => modifiers[mod])
.reduce((acc, val) => acc + val, 0); .reduce((acc, val) => acc + val, 0);
// If the step has keys and/or modifiers, press them and hold for the delay // If the step has keys and/or modifiers, press them and hold for the delay
@ -306,6 +303,7 @@ export default function useKeyboard() {
sendKeyboardMacroEventHidRpc(macro); sendKeyboardMacroEventHidRpc(macro);
}, [sendKeyboardMacroEventHidRpc]); }, [sendKeyboardMacroEventHidRpc]);
const executeMacroClientSide = useCallback(async (steps: MacroSteps) => { const executeMacroClientSide = useCallback(async (steps: MacroSteps) => {
const promises: (() => Promise<void>)[] = []; const promises: (() => Promise<void>)[] = [];

2
web.go
View File

@ -230,7 +230,7 @@ func handleWebRTCSession(c *gin.Context) {
} }
// Cancel any ongoing keyboard macro when session changes // Cancel any ongoing keyboard macro when session changes
cancelKeyboardMacro() cancelAllRunningKeyboardMacros()
currentSession = session currentSession = session
c.JSON(http.StatusOK, gin.H{"sd": sd}) c.JSON(http.StatusOK, gin.H{"sd": sd})

View File

@ -34,7 +34,7 @@ type Session struct {
lastTimerResetTime time.Time // Track when auto-release timer was last reset lastTimerResetTime time.Time // Track when auto-release timer was last reset
keepAliveJitterLock sync.Mutex // Protect jitter compensation timing state keepAliveJitterLock sync.Mutex // Protect jitter compensation timing state
hidQueueLock sync.Mutex hidQueueLock sync.Mutex
hidQueue []chan hidQueueMessage hidQueues []chan hidQueueMessage
keysDownStateQueue chan usbgadget.KeysDownState keysDownStateQueue chan usbgadget.KeysDownState
} }
@ -48,7 +48,8 @@ func (s *Session) resetKeepAliveTime() {
type hidQueueMessage struct { type hidQueueMessage struct {
webrtc.DataChannelMessage webrtc.DataChannelMessage
channel string channel string
timelimit time.Duration
} }
type SessionConfig struct { type SessionConfig struct {
@ -93,19 +94,20 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) {
return base64.StdEncoding.EncodeToString(localDescription), nil return base64.StdEncoding.EncodeToString(localDescription), nil
} }
func (s *Session) initQueues() { func (s *Session) initHidQueues() {
s.hidQueueLock.Lock() s.hidQueueLock.Lock()
defer s.hidQueueLock.Unlock() defer s.hidQueueLock.Unlock()
s.hidQueue = make([]chan hidQueueMessage, 0) s.hidQueues = make([]chan hidQueueMessage, hidrpc.OtherQueue+1)
for i := 0; i < 4; i++ { s.hidQueues[hidrpc.HandshakeQueue] = make(chan hidQueueMessage, 2) // we don't really want to queue many handshake messages
q := make(chan hidQueueMessage, 256) s.hidQueues[hidrpc.KeyboardQueue] = make(chan hidQueueMessage, 256)
s.hidQueue = append(s.hidQueue, q) s.hidQueues[hidrpc.MouseQueue] = make(chan hidQueueMessage, 256)
} s.hidQueues[hidrpc.MacroQueue] = make(chan hidQueueMessage, 10) // macros can be long, but we don't want to queue too many
s.hidQueues[hidrpc.OtherQueue] = make(chan hidQueueMessage, 256)
} }
func (s *Session) handleQueues(index int) { func (s *Session) handleQueue(queue chan hidQueueMessage) {
for msg := range s.hidQueue[index] { for msg := range queue {
onHidMessage(msg, s) onHidMessage(msg, s)
} }
} }
@ -160,17 +162,18 @@ func getOnHidMessageHandler(session *Session, scopedLogger *zerolog.Logger, chan
l.Trace().Msg("received data in HID RPC message handler") l.Trace().Msg("received data in HID RPC message handler")
// Enqueue to ensure ordered processing // Enqueue to ensure ordered processing
queueIndex := hidrpc.GetQueueIndex(hidrpc.MessageType(msg.Data[0])) queueIndex, timelimit := hidrpc.GetQueueIndex(hidrpc.MessageType(msg.Data[0]))
if queueIndex >= len(session.hidQueue) || queueIndex < 0 { if queueIndex >= len(session.hidQueues) || queueIndex < 0 {
l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue index not found") l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue index not found")
queueIndex = 3 queueIndex = hidrpc.OtherQueue
} }
queue := session.hidQueue[queueIndex] queue := session.hidQueues[queueIndex]
if queue != nil { if queue != nil {
queue <- hidQueueMessage{ queue <- hidQueueMessage{
DataChannelMessage: msg, DataChannelMessage: msg,
channel: channel, channel: channel,
timelimit: timelimit,
} }
} else { } else {
l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue is nil") l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue is nil")
@ -220,7 +223,7 @@ func newSession(config SessionConfig) (*Session, error) {
session := &Session{peerConnection: peerConnection} session := &Session{peerConnection: peerConnection}
session.rpcQueue = make(chan webrtc.DataChannelMessage, 256) session.rpcQueue = make(chan webrtc.DataChannelMessage, 256)
session.initQueues() session.initHidQueues()
session.initKeysDownStateQueue() session.initKeysDownStateQueue()
go func() { go func() {
@ -230,8 +233,8 @@ func newSession(config SessionConfig) (*Session, error) {
} }
}() }()
for i := 0; i < len(session.hidQueue); i++ { for queue := range session.hidQueues {
go session.handleQueues(i) go session.handleQueue(session.hidQueues[queue])
} }
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
@ -256,7 +259,11 @@ func newSession(config SessionConfig) (*Session, error) {
session.RPCChannel = d session.RPCChannel = d
d.OnMessage(func(msg webrtc.DataChannelMessage) { d.OnMessage(func(msg webrtc.DataChannelMessage) {
// Enqueue to ensure ordered processing // Enqueue to ensure ordered processing
session.rpcQueue <- msg if session.rpcQueue != nil {
session.rpcQueue <- msg
} else {
scopedLogger.Warn().Msg("RPC message received but rpcQueue is nil")
}
}) })
triggerOTAStateUpdate() triggerOTAStateUpdate()
triggerVideoStateUpdate() triggerVideoStateUpdate()
@ -325,22 +332,23 @@ func newSession(config SessionConfig) (*Session, error) {
_ = peerConnection.Close() _ = peerConnection.Close()
} }
if connectionState == webrtc.ICEConnectionStateClosed { if connectionState == webrtc.ICEConnectionStateClosed {
scopedLogger.Debug().Msg("ICE Connection State is closed, unmounting virtual media") scopedLogger.Debug().Msg("ICE Connection State is closed, tearing down session")
if session == currentSession { if session == currentSession {
// Cancel any ongoing keyboard report multi when session closes // Cancel any ongoing keyboard report multi when session closes
cancelKeyboardMacro() cancelAllRunningKeyboardMacros()
currentSession = nil currentSession = nil
} }
// Stop RPC processor // Stop RPC processor
if session.rpcQueue != nil { if session.rpcQueue != nil {
close(session.rpcQueue) close(session.rpcQueue)
session.rpcQueue = nil session.rpcQueue = nil
} }
// Stop HID RPC processor // Stop HID RPC processors
for i := 0; i < len(session.hidQueue); i++ { for i := 0; i < len(session.hidQueues); i++ {
close(session.hidQueue[i]) close(session.hidQueues[i])
session.hidQueue[i] = nil session.hidQueues[i] = nil
} }
close(session.keysDownStateQueue) close(session.keysDownStateQueue)