diff --git a/cloud.go b/cloud.go index cec749e4..f86a4815 100644 --- a/cloud.go +++ b/cloud.go @@ -475,6 +475,10 @@ func handleSessionRequest( cloudLogger.Info().Interface("session", session).Msg("new session accepted") cloudLogger.Trace().Interface("session", session).Msg("new session accepted") + + // Cancel any ongoing keyboard report multi when session changes + cancelKeyboardReportMulti() + currentSession = session _ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd}) return nil diff --git a/hidrpc.go b/hidrpc.go index 74fe687f..604be89f 100644 --- a/hidrpc.go +++ b/hidrpc.go @@ -1,7 +1,10 @@ package kvm import ( + "context" + "errors" "fmt" + "io" "time" "github.com/jetkvm/kvm/internal/hidrpc" @@ -29,6 +32,13 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) { session.reportHidRPCKeysDownState(*keysDownState) } rpcErr = err + case hidrpc.TypeKeyboardMacroReport: + keyboardMacroReport, err := message.KeyboardMacroReport() + if err != nil { + logger.Warn().Err(err).Msg("failed to get keyboard macro report") + return + } + _, rpcErr = rpcKeyboardReportMulti(context.Background(), keyboardMacroReport.Macro) case hidrpc.TypePointerReport: pointerReport, err := message.PointerReport() if err != nil { @@ -143,6 +153,10 @@ func reportHidRPC(params any, session *Session) { } 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") } } diff --git a/internal/hidrpc/hidrpc.go b/internal/hidrpc/hidrpc.go index e9c8c24d..c4d99615 100644 --- a/internal/hidrpc/hidrpc.go +++ b/internal/hidrpc/hidrpc.go @@ -10,14 +10,17 @@ import ( type MessageType byte const ( - TypeHandshake MessageType = 0x01 - TypeKeyboardReport MessageType = 0x02 - TypePointerReport MessageType = 0x03 - TypeWheelReport MessageType = 0x04 - TypeKeypressReport MessageType = 0x05 - TypeMouseReport MessageType = 0x06 - TypeKeyboardLedState MessageType = 0x32 - TypeKeydownState MessageType = 0x33 + TypeHandshake MessageType = 0x01 + TypeKeyboardReport MessageType = 0x02 + TypePointerReport MessageType = 0x03 + TypeWheelReport MessageType = 0x04 + TypeKeypressReport MessageType = 0x05 + TypeMouseReport MessageType = 0x06 + TypeKeyboardMacroReport MessageType = 0x07 + TypeCancelKeyboardMacroReport MessageType = 0x08 + TypeKeyboardLedState MessageType = 0x32 + TypeKeydownState MessageType = 0x33 + TypeKeyboardMacroStateReport MessageType = 0x34 ) const ( @@ -29,7 +32,7 @@ func GetQueueIndex(messageType MessageType) int { switch messageType { case TypeHandshake: return 0 - case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardLedState, TypeKeydownState: + case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardLedState, TypeKeydownState, TypeKeyboardMacroStateReport: return 1 case TypePointerReport, TypeMouseReport, TypeWheelReport: return 2 diff --git a/internal/hidrpc/message.go b/internal/hidrpc/message.go index 84bbda7c..fa403265 100644 --- a/internal/hidrpc/message.go +++ b/internal/hidrpc/message.go @@ -1,6 +1,7 @@ package hidrpc import ( + "encoding/binary" "fmt" ) @@ -43,6 +44,11 @@ func (m *Message) String() string { return fmt.Sprintf("MouseReport{Malformed: %v}", m.d) } return fmt.Sprintf("MouseReport{DX: %d, DY: %d, Button: %d}", m.d[0], m.d[1], m.d[2]) + case TypeKeyboardMacroReport: + if len(m.d) < 5 { + 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])) default: return fmt.Sprintf("Unknown{Type: %d, Data: %v}", m.t, m.d) } @@ -84,6 +90,51 @@ func (m *Message) KeyboardReport() (KeyboardReport, error) { }, nil } +// Macro .. +type KeyboardMacro struct { + Modifier byte // 1 byte + Keys []byte // 6 bytes, to make things easier, the keys length is fixed to 6 + Delay uint16 // 2 bytes +} +type KeyboardMacroReport struct { + IsPaste bool + Length uint32 + Macro []KeyboardMacro +} + +// KeyboardMacroReport returns the keyboard macro report from the message. +func (m *Message) KeyboardMacroReport() (KeyboardMacroReport, error) { + if m.t != TypeKeyboardMacroReport { + return KeyboardMacroReport{}, fmt.Errorf("invalid message type: %d", m.t) + } + + isPaste := m.d[0] == uint8(1) + length := binary.BigEndian.Uint32(m.d[1:5]) + + // check total length + expectedLength := int(length)*9 + 5 + if len(m.d) != expectedLength { + return KeyboardMacroReport{}, fmt.Errorf("invalid length: %d, expected: %d", len(m.d), expectedLength) + } + + macro := make([]KeyboardMacro, 0, int(length)) + for i := 0; i < int(length); i++ { + offset := 5 + i*9 + + macro = append(macro, KeyboardMacro{ + Modifier: m.d[offset], + Keys: m.d[offset+1 : offset+7], + Delay: binary.BigEndian.Uint16(m.d[offset+7 : offset+9]), + }) + } + + return KeyboardMacroReport{ + IsPaste: isPaste, + Macro: macro, + Length: length, + }, nil +} + // PointerReport .. type PointerReport struct { X int diff --git a/jsonrpc.go b/jsonrpc.go index ff3a4b12..911793b3 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -16,6 +16,7 @@ import ( "github.com/rs/zerolog" "go.bug.st/serial" + "github.com/jetkvm/kvm/internal/hidrpc" "github.com/jetkvm/kvm/internal/usbgadget" ) @@ -1049,17 +1050,105 @@ func rpcSetLocalLoopbackOnly(enabled bool) error { return nil } +func cancelKeyboardReportMulti() { + +} + +// // cancelKeyboardReportMulti cancels any ongoing keyboard report multi execution +// func cancelKeyboardReportMulti() { +// keyboardReportMultiLock.Lock() +// defer keyboardReportMultiLock.Unlock() + +// if keyboardReportMultiCancel != nil { +// keyboardReportMultiCancel() +// logger.Info().Msg("canceled keyboard report multi") +// keyboardReportMultiCancel = nil +// } +// } + +// func setKeyboardReportMultiCancel(cancel context.CancelFunc) { +// keyboardReportMultiLock.Lock() +// defer keyboardReportMultiLock.Unlock() + +// keyboardReportMultiCancel = cancel +// } + +// func rpcKeyboardReportMultiWrapper(macro []map[string]any) (usbgadget.KeysDownState, error) { +// // cancelKeyboardReportMulti() + +// // ctx, cancel := context.WithCancel(context.Background()) +// // setKeyboardReportMultiCancel(cancel) + +// // writeJSONRPCEvent("keyboardReportMultiState", true, currentSession) + +// // result, err := rpcKeyboardReportMulti(ctx, macro) + +// // setKeyboardReportMultiCancel(nil) + +// // writeJSONRPCEvent("keyboardReportMultiState", false, currentSession) + +// // return result, err +// } + +// var ( +// keyboardReportMultiCancel context.CancelFunc +// keyboardReportMultiLock sync.Mutex +// ) + +// func rpcCancelKeyboardReportMulti() { +// cancelKeyboardReportMulti() +// } + +func rpcKeyboardReportMulti(ctx context.Context, macro []hidrpc.KeyboardMacro) (usbgadget.KeysDownState, error) { + var last usbgadget.KeysDownState + var err error + + logger.Debug().Interface("macro", macro).Msg("Executing keyboard report multi") + + for i, step := range macro { + // Check for cancellation before each step + select { + case <-ctx.Done(): + logger.Debug().Msg("Keyboard report multi context cancelled") + return last, ctx.Err() + default: + } + + delay := time.Duration(step.Delay) * time.Millisecond + logger.Info().Int("step", i).Uint16("delay", step.Delay).Msg("Keyboard report multi delay") + + last, err = rpcKeyboardReport(step.Modifier, step.Keys) + if err != nil { + logger.Warn().Err(err).Msg("failed to execute keyboard report multi") + return last, err + } + + // Use context-aware sleep that can be cancelled + select { + case <-time.After(delay): + // Sleep completed normally + case <-ctx.Done(): + logger.Debug().Int("step", i).Msg("Keyboard report multi cancelled during sleep") + return last, ctx.Err() + } + } + + return last, nil +} + var rpcHandlers = map[string]RPCHandler{ - "ping": {Func: rpcPing}, - "reboot": {Func: rpcReboot, Params: []string{"force"}}, - "getDeviceID": {Func: rpcGetDeviceID}, - "deregisterDevice": {Func: rpcDeregisterDevice}, - "getCloudState": {Func: rpcGetCloudState}, - "getNetworkState": {Func: rpcGetNetworkState}, - "getNetworkSettings": {Func: rpcGetNetworkSettings}, - "setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}}, - "renewDHCPLease": {Func: rpcRenewDHCPLease}, - "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, + "ping": {Func: rpcPing}, + "reboot": {Func: rpcReboot, Params: []string{"force"}}, + "getDeviceID": {Func: rpcGetDeviceID}, + "deregisterDevice": {Func: rpcDeregisterDevice}, + "getCloudState": {Func: rpcGetCloudState}, + "getNetworkState": {Func: rpcGetNetworkState}, + "getNetworkSettings": {Func: rpcGetNetworkSettings}, + "setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}}, + "renewDHCPLease": {Func: rpcRenewDHCPLease}, + "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, + // "keyboardReportMulti": {Func: rpcKeyboardReportMultiWrapper, Params: []string{"macro"}}, + // "cancelKeyboardReportMulti": {Func: rpcCancelKeyboardReportMulti}, "getKeyboardLedState": {Func: rpcGetKeyboardLedState}, "keypressReport": {Func: rpcKeypressReport, Params: []string{"key", "press"}}, "getKeyDownState": {Func: rpcGetKeysDownState}, diff --git a/ui/src/components/InfoBar.tsx b/ui/src/components/InfoBar.tsx index 8d0b2822..62759d17 100644 --- a/ui/src/components/InfoBar.tsx +++ b/ui/src/components/InfoBar.tsx @@ -27,6 +27,7 @@ export default function InfoBar() { const { rpcDataChannel } = useRTCStore(); const { debugMode, mouseMode, showPressedKeys } = useSettingsStore(); + const { isPasteModeEnabled } = useHidStore(); useEffect(() => { if (!rpcDataChannel) return; @@ -108,7 +109,12 @@ export default function InfoBar() { {rpcHidStatus} )} - + {isPasteModeEnabled && ( +
- Sending text using keyboard layout: {selectedKeyboard.isoCode}-{selectedKeyboard.name} + Sending text using keyboard layout: {selectedKeyboard.isoCode}- + {selectedKeyboard.name}