Skip to content

Commit 72e3013

Browse files
ymadamshiervani
andauthored
feat: send all paste keystrokes to backend (#789)
* feat: send all paste keystrokes to backend * feat: cancel paste mode * wip: send macro using hidRPC channel * add delay * feat: allow paste progress to be cancelled * allow user to override delay * chore: clear keysDownState * fix: use currentSession.reportHidRPCKeyboardMacroState * fix: jsonrpc.go:1142:21: Error return value is not checked (errcheck) * fix: performance issue of Uint8Array concat * chore: hide delay option when debugMode isn't enabled * feat: use clientSide macro if backend doesn't support macros * fix: update keysDownState handling * minor issues * refactor * fix: send duplicated keyDownState * chore: add max length for paste text --------- Co-authored-by: Adam Shiervani <[email protected]>
1 parent 25b102a commit 72e3013

File tree

14 files changed

+701
-141
lines changed

14 files changed

+701
-141
lines changed

cloud.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,10 @@ func handleSessionRequest(
475475

476476
cloudLogger.Info().Interface("session", session).Msg("new session accepted")
477477
cloudLogger.Trace().Interface("session", session).Msg("new session accepted")
478+
479+
// Cancel any ongoing keyboard macro when session changes
480+
cancelKeyboardMacro()
481+
478482
currentSession = session
479483
_ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd})
480484
return nil

hidrpc.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package kvm
22

33
import (
4+
"errors"
45
"fmt"
6+
"io"
57
"time"
68

79
"github.com/jetkvm/kvm/internal/hidrpc"
@@ -29,6 +31,16 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
2931
session.reportHidRPCKeysDownState(*keysDownState)
3032
}
3133
rpcErr = err
34+
case hidrpc.TypeKeyboardMacroReport:
35+
keyboardMacroReport, err := message.KeyboardMacroReport()
36+
if err != nil {
37+
logger.Warn().Err(err).Msg("failed to get keyboard macro report")
38+
return
39+
}
40+
_, rpcErr = rpcExecuteKeyboardMacro(keyboardMacroReport.Steps)
41+
case hidrpc.TypeCancelKeyboardMacroReport:
42+
rpcCancelKeyboardMacro()
43+
return
3244
case hidrpc.TypePointerReport:
3345
pointerReport, err := message.PointerReport()
3446
if err != nil {
@@ -128,6 +140,8 @@ func reportHidRPC(params any, session *Session) {
128140
message, err = hidrpc.NewKeyboardLedMessage(params).Marshal()
129141
case usbgadget.KeysDownState:
130142
message, err = hidrpc.NewKeydownStateMessage(params).Marshal()
143+
case hidrpc.KeyboardMacroState:
144+
message, err = hidrpc.NewKeyboardMacroStateMessage(params.State, params.IsPaste).Marshal()
131145
default:
132146
err = fmt.Errorf("unknown HID RPC message type: %T", params)
133147
}
@@ -143,6 +157,10 @@ func reportHidRPC(params any, session *Session) {
143157
}
144158

145159
if err := session.HidChannel.Send(message); err != nil {
160+
if errors.Is(err, io.ErrClosedPipe) {
161+
logger.Debug().Err(err).Msg("HID RPC channel closed, skipping reportHidRPC")
162+
return
163+
}
146164
logger.Warn().Err(err).Msg("failed to send HID RPC message")
147165
}
148166
}
@@ -160,3 +178,10 @@ func (s *Session) reportHidRPCKeysDownState(state usbgadget.KeysDownState) {
160178
}
161179
reportHidRPC(state, s)
162180
}
181+
182+
func (s *Session) reportHidRPCKeyboardMacroState(state hidrpc.KeyboardMacroState) {
183+
if !s.hidRPCAvailable {
184+
writeJSONRPCEvent("keyboardMacroState", state, s)
185+
}
186+
reportHidRPC(state, s)
187+
}

internal/hidrpc/hidrpc.go

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,17 @@ import (
1010
type MessageType byte
1111

1212
const (
13-
TypeHandshake MessageType = 0x01
14-
TypeKeyboardReport MessageType = 0x02
15-
TypePointerReport MessageType = 0x03
16-
TypeWheelReport MessageType = 0x04
17-
TypeKeypressReport MessageType = 0x05
18-
TypeMouseReport MessageType = 0x06
19-
TypeKeyboardLedState MessageType = 0x32
20-
TypeKeydownState MessageType = 0x33
13+
TypeHandshake MessageType = 0x01
14+
TypeKeyboardReport MessageType = 0x02
15+
TypePointerReport MessageType = 0x03
16+
TypeWheelReport MessageType = 0x04
17+
TypeKeypressReport MessageType = 0x05
18+
TypeMouseReport MessageType = 0x06
19+
TypeKeyboardMacroReport MessageType = 0x07
20+
TypeCancelKeyboardMacroReport MessageType = 0x08
21+
TypeKeyboardLedState MessageType = 0x32
22+
TypeKeydownState MessageType = 0x33
23+
TypeKeyboardMacroState MessageType = 0x34
2124
)
2225

2326
const (
@@ -29,10 +32,13 @@ func GetQueueIndex(messageType MessageType) int {
2932
switch messageType {
3033
case TypeHandshake:
3134
return 0
32-
case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardLedState, TypeKeydownState:
35+
case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardMacroReport, TypeKeyboardLedState, TypeKeydownState, TypeKeyboardMacroState:
3336
return 1
3437
case TypePointerReport, TypeMouseReport, TypeWheelReport:
3538
return 2
39+
// we don't want to block the queue for this message
40+
case TypeCancelKeyboardMacroReport:
41+
return 3
3642
default:
3743
return 3
3844
}
@@ -98,3 +104,19 @@ func NewKeydownStateMessage(state usbgadget.KeysDownState) *Message {
98104
d: data,
99105
}
100106
}
107+
108+
// NewKeyboardMacroStateMessage creates a new keyboard macro state message.
109+
func NewKeyboardMacroStateMessage(state bool, isPaste bool) *Message {
110+
data := make([]byte, 2)
111+
if state {
112+
data[0] = 1
113+
}
114+
if isPaste {
115+
data[1] = 1
116+
}
117+
118+
return &Message{
119+
t: TypeKeyboardMacroState,
120+
d: data,
121+
}
122+
}

internal/hidrpc/message.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package hidrpc
22

33
import (
4+
"encoding/binary"
45
"fmt"
56
)
67

@@ -43,6 +44,11 @@ func (m *Message) String() string {
4344
return fmt.Sprintf("MouseReport{Malformed: %v}", m.d)
4445
}
4546
return fmt.Sprintf("MouseReport{DX: %d, DY: %d, Button: %d}", m.d[0], m.d[1], m.d[2])
47+
case TypeKeyboardMacroReport:
48+
if len(m.d) < 5 {
49+
return fmt.Sprintf("KeyboardMacroReport{Malformed: %v}", m.d)
50+
}
51+
return fmt.Sprintf("KeyboardMacroReport{IsPaste: %v, Length: %d}", m.d[0] == uint8(1), binary.BigEndian.Uint32(m.d[1:5]))
4652
default:
4753
return fmt.Sprintf("Unknown{Type: %d, Data: %v}", m.t, m.d)
4854
}
@@ -84,6 +90,55 @@ func (m *Message) KeyboardReport() (KeyboardReport, error) {
8490
}, nil
8591
}
8692

93+
// Macro ..
94+
type KeyboardMacroStep struct {
95+
Modifier byte // 1 byte
96+
Keys []byte // 6 bytes: hidKeyBufferSize
97+
Delay uint16 // 2 bytes
98+
}
99+
type KeyboardMacroReport struct {
100+
IsPaste bool
101+
StepCount uint32
102+
Steps []KeyboardMacroStep
103+
}
104+
105+
// HidKeyBufferSize is the size of the keys buffer in the keyboard report.
106+
const HidKeyBufferSize = 6
107+
108+
// KeyboardMacroReport returns the keyboard macro report from the message.
109+
func (m *Message) KeyboardMacroReport() (KeyboardMacroReport, error) {
110+
if m.t != TypeKeyboardMacroReport {
111+
return KeyboardMacroReport{}, fmt.Errorf("invalid message type: %d", m.t)
112+
}
113+
114+
isPaste := m.d[0] == uint8(1)
115+
stepCount := binary.BigEndian.Uint32(m.d[1:5])
116+
117+
// check total length
118+
expectedLength := int(stepCount)*9 + 5
119+
if len(m.d) != expectedLength {
120+
return KeyboardMacroReport{}, fmt.Errorf("invalid length: %d, expected: %d", len(m.d), expectedLength)
121+
}
122+
123+
steps := make([]KeyboardMacroStep, 0, int(stepCount))
124+
offset := 5
125+
for i := 0; i < int(stepCount); i++ {
126+
steps = append(steps, KeyboardMacroStep{
127+
Modifier: m.d[offset],
128+
Keys: m.d[offset+1 : offset+7],
129+
Delay: binary.BigEndian.Uint16(m.d[offset+7 : offset+9]),
130+
})
131+
132+
offset += 1 + HidKeyBufferSize + 2
133+
}
134+
135+
return KeyboardMacroReport{
136+
IsPaste: isPaste,
137+
Steps: steps,
138+
StepCount: stepCount,
139+
}, nil
140+
}
141+
87142
// PointerReport ..
88143
type PointerReport struct {
89144
X int
@@ -131,3 +186,20 @@ func (m *Message) MouseReport() (MouseReport, error) {
131186
Button: uint8(m.d[2]),
132187
}, nil
133188
}
189+
190+
type KeyboardMacroState struct {
191+
State bool
192+
IsPaste bool
193+
}
194+
195+
// KeyboardMacroState returns the keyboard macro state report from the message.
196+
func (m *Message) KeyboardMacroState() (KeyboardMacroState, error) {
197+
if m.t != TypeKeyboardMacroState {
198+
return KeyboardMacroState{}, fmt.Errorf("invalid message type: %d", m.t)
199+
}
200+
201+
return KeyboardMacroState{
202+
State: m.d[0] == uint8(1),
203+
IsPaste: m.d[1] == uint8(1),
204+
}, nil
205+
}

jsonrpc.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package kvm
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/json"
67
"errors"
@@ -10,12 +11,14 @@ import (
1011
"path/filepath"
1112
"reflect"
1213
"strconv"
14+
"sync"
1315
"time"
1416

1517
"github.com/pion/webrtc/v4"
1618
"github.com/rs/zerolog"
1719
"go.bug.st/serial"
1820

21+
"github.com/jetkvm/kvm/internal/hidrpc"
1922
"github.com/jetkvm/kvm/internal/usbgadget"
2023
"github.com/jetkvm/kvm/internal/utils"
2124
)
@@ -1056,6 +1059,106 @@ func rpcSetLocalLoopbackOnly(enabled bool) error {
10561059
return nil
10571060
}
10581061

1062+
var (
1063+
keyboardMacroCancel context.CancelFunc
1064+
keyboardMacroLock sync.Mutex
1065+
)
1066+
1067+
// cancelKeyboardMacro cancels any ongoing keyboard macro execution
1068+
func cancelKeyboardMacro() {
1069+
keyboardMacroLock.Lock()
1070+
defer keyboardMacroLock.Unlock()
1071+
1072+
if keyboardMacroCancel != nil {
1073+
keyboardMacroCancel()
1074+
logger.Info().Msg("canceled keyboard macro")
1075+
keyboardMacroCancel = nil
1076+
}
1077+
}
1078+
1079+
func setKeyboardMacroCancel(cancel context.CancelFunc) {
1080+
keyboardMacroLock.Lock()
1081+
defer keyboardMacroLock.Unlock()
1082+
1083+
keyboardMacroCancel = cancel
1084+
}
1085+
1086+
func rpcExecuteKeyboardMacro(macro []hidrpc.KeyboardMacroStep) (usbgadget.KeysDownState, error) {
1087+
cancelKeyboardMacro()
1088+
1089+
ctx, cancel := context.WithCancel(context.Background())
1090+
setKeyboardMacroCancel(cancel)
1091+
1092+
s := hidrpc.KeyboardMacroState{
1093+
State: true,
1094+
IsPaste: true,
1095+
}
1096+
1097+
if currentSession != nil {
1098+
currentSession.reportHidRPCKeyboardMacroState(s)
1099+
}
1100+
1101+
result, err := rpcDoExecuteKeyboardMacro(ctx, macro)
1102+
1103+
setKeyboardMacroCancel(nil)
1104+
1105+
s.State = false
1106+
if currentSession != nil {
1107+
currentSession.reportHidRPCKeyboardMacroState(s)
1108+
}
1109+
1110+
return result, err
1111+
}
1112+
1113+
func rpcCancelKeyboardMacro() {
1114+
cancelKeyboardMacro()
1115+
}
1116+
1117+
var keyboardClearStateKeys = make([]byte, hidrpc.HidKeyBufferSize)
1118+
1119+
func isClearKeyStep(step hidrpc.KeyboardMacroStep) bool {
1120+
return step.Modifier == 0 && bytes.Equal(step.Keys, keyboardClearStateKeys)
1121+
}
1122+
1123+
func rpcDoExecuteKeyboardMacro(ctx context.Context, macro []hidrpc.KeyboardMacroStep) (usbgadget.KeysDownState, error) {
1124+
var last usbgadget.KeysDownState
1125+
var err error
1126+
1127+
logger.Debug().Interface("macro", macro).Msg("Executing keyboard macro")
1128+
1129+
for i, step := range macro {
1130+
delay := time.Duration(step.Delay) * time.Millisecond
1131+
1132+
last, err = rpcKeyboardReport(step.Modifier, step.Keys)
1133+
if err != nil {
1134+
logger.Warn().Err(err).Msg("failed to execute keyboard macro")
1135+
return last, err
1136+
}
1137+
1138+
// notify the device that the keyboard state is being cleared
1139+
if isClearKeyStep(step) {
1140+
gadget.UpdateKeysDown(0, keyboardClearStateKeys)
1141+
}
1142+
1143+
// Use context-aware sleep that can be cancelled
1144+
select {
1145+
case <-time.After(delay):
1146+
// Sleep completed normally
1147+
case <-ctx.Done():
1148+
// make sure keyboard state is reset
1149+
_, err := rpcKeyboardReport(0, keyboardClearStateKeys)
1150+
if err != nil {
1151+
logger.Warn().Err(err).Msg("failed to reset keyboard state")
1152+
}
1153+
1154+
logger.Debug().Int("step", i).Msg("Keyboard macro cancelled during sleep")
1155+
return last, ctx.Err()
1156+
}
1157+
}
1158+
1159+
return last, nil
1160+
}
1161+
10591162
var rpcHandlers = map[string]RPCHandler{
10601163
"ping": {Func: rpcPing},
10611164
"reboot": {Func: rpcReboot, Params: []string{"force"}},

ui/src/components/InfoBar.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export default function InfoBar() {
2727

2828
const { rpcDataChannel } = useRTCStore();
2929
const { debugMode, mouseMode, showPressedKeys } = useSettingsStore();
30+
const { isPasteInProgress } = useHidStore();
3031

3132
useEffect(() => {
3233
if (!rpcDataChannel) return;
@@ -108,7 +109,12 @@ export default function InfoBar() {
108109
<span className="text-xs">{rpcHidStatus}</span>
109110
</div>
110111
)}
111-
112+
{isPasteInProgress && (
113+
<div className="flex w-[156px] items-center gap-x-1">
114+
<span className="text-xs font-semibold">Paste Mode:</span>
115+
<span className="text-xs">Enabled</span>
116+
</div>
117+
)}
112118
{showPressedKeys && (
113119
<div className="flex items-center gap-x-1">
114120
<span className="text-xs font-semibold">Keys:</span>

0 commit comments

Comments
 (0)