mirror of https://github.com/jetkvm/kvm.git
Merge e89491a513 into 28919bf37c
This commit is contained in:
commit
2ff93e7342
2
cloud.go
2
cloud.go
|
|
@ -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})
|
||||||
|
|
|
||||||
42
hidrpc.go
42
hidrpc.go
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 * time.Second
|
||||||
case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardMacroReport, TypeKeyboardLedState, TypeKeydownState, TypeKeyboardMacroState:
|
case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardLedState, TypeKeydownState, TypeKeyboardMacroState:
|
||||||
return 1
|
return KeyboardQueue, 1 * time.Second
|
||||||
case TypePointerReport, TypeMouseReport, TypeWheelReport:
|
case TypePointerReport, TypeMouseReport, TypeWheelReport:
|
||||||
return 2
|
return MouseQueue, 1 * time.Second
|
||||||
// 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 * time.Second
|
||||||
default:
|
default:
|
||||||
return 3
|
return OtherQueue, 5 * time.Second
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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: TypeKeyboardMacroTokenState,
|
||||||
|
d: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ package hidrpc
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Message ..
|
// Message ..
|
||||||
|
|
@ -23,6 +26,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 +51,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 +106,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,18 +136,16 @@ 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: usbgadget.HidKeyBufferSize
|
||||||
Delay uint16 // 2 bytes
|
Delay uint16 // 2 bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeyboardMacroReport struct {
|
type KeyboardMacroReport struct {
|
||||||
IsPaste bool
|
IsPaste bool
|
||||||
StepCount uint32
|
StepCount uint32
|
||||||
Steps []KeyboardMacroStep
|
Steps []KeyboardMacroStep
|
||||||
}
|
}
|
||||||
|
|
||||||
// HidKeyBufferSize is the size of the keys buffer in the keyboard report.
|
|
||||||
const HidKeyBufferSize = 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) {
|
||||||
if m.t != TypeKeyboardMacroReport {
|
if m.t != TypeKeyboardMacroReport {
|
||||||
|
|
@ -131,7 +170,7 @@ func (m *Message) KeyboardMacroReport() (KeyboardMacroReport, error) {
|
||||||
Delay: binary.BigEndian.Uint16(m.d[offset+7 : offset+9]),
|
Delay: binary.BigEndian.Uint16(m.d[offset+7 : offset+9]),
|
||||||
})
|
})
|
||||||
|
|
||||||
offset += 1 + HidKeyBufferSize + 2
|
offset += 1 + usbgadget.HidKeyBufferSize + 2
|
||||||
}
|
}
|
||||||
|
|
||||||
return KeyboardMacroReport{
|
return KeyboardMacroReport{
|
||||||
|
|
@ -205,3 +244,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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,44 +28,55 @@ var keyboardConfig = gadgetConfigItem{
|
||||||
|
|
||||||
// Source: https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt
|
// Source: https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt
|
||||||
var keyboardReportDesc = []byte{
|
var keyboardReportDesc = []byte{
|
||||||
0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */
|
/* boot mode descriptor */
|
||||||
0x09, 0x06, /* USAGE (Keyboard) */
|
0x05, 0x01, /* USAGE_PAGE-global (Generic Desktop) */
|
||||||
0xa1, 0x01, /* COLLECTION (Application) */
|
0x09, 0x06, /* USAGE-local (Keyboard) */
|
||||||
0x05, 0x07, /* USAGE_PAGE (Keyboard) */
|
0xA1, 0x01, /* COLLECTION-main (Application) */
|
||||||
0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */
|
|
||||||
0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */
|
|
||||||
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
|
|
||||||
0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
|
|
||||||
0x75, 0x01, /* REPORT_SIZE (1) */
|
|
||||||
0x95, 0x08, /* REPORT_COUNT (8) */
|
|
||||||
0x81, 0x02, /* INPUT (Data,Var,Abs) */
|
|
||||||
0x95, 0x01, /* REPORT_COUNT (1) */
|
|
||||||
0x75, 0x08, /* REPORT_SIZE (8) */
|
|
||||||
0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
|
|
||||||
0x95, 0x05, /* REPORT_COUNT (5) */
|
|
||||||
0x75, 0x01, /* REPORT_SIZE (1) */
|
|
||||||
|
|
||||||
0x05, 0x08, /* USAGE_PAGE (LEDs) */
|
/* 8 modifier bits */
|
||||||
0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */
|
0x05, 0x07, /* USAGE_PAGE-global (Keyboard) */
|
||||||
0x29, 0x05, /* USAGE_MAXIMUM (Kana) */
|
0x19, 0xe0, /* USAGE_MINIMUM-local 0xE0 (Keyboard LeftControl) */
|
||||||
0x91, 0x02, /* OUTPUT (Data,Var,Abs) */
|
0x29, 0xe7, /* USAGE_MAXIMUM-local 0xE7 (Keyboard Right GUI) */
|
||||||
0x95, 0x01, /* REPORT_COUNT (1) */
|
0x15, 0x00, /* LOGICAL_MINIMUM-global (0) Modifier bit off) */
|
||||||
0x75, 0x03, /* REPORT_SIZE (3) */
|
0x25, 0x01, /* LOGICAL_MAXIMUM-global (1) Modifier bit on) */
|
||||||
0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */
|
0x75, 0x01, /* REPORT_SIZE-global (1) one bit per modifier */
|
||||||
0x95, 0x06, /* REPORT_COUNT (6) */
|
0x95, 0x08, /* REPORT_COUNT-global (8) 8 total bits */
|
||||||
0x75, 0x08, /* REPORT_SIZE (8) */
|
0x81, 0x02, /* INPUT-main (Data,Var,Abs) Modifier bits 0-7 */
|
||||||
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
|
|
||||||
0x25, 0x65, /* LOGICAL_MAXIMUM (101) */
|
/* 8 bits of padding */
|
||||||
0x05, 0x07, /* USAGE_PAGE (Keyboard) */
|
0x95, 0x01, /* REPORT_COUNT-global (1) one field */
|
||||||
0x19, 0x00, /* USAGE_MINIMUM (Reserved) */
|
0x75, 0x08, /* REPORT_SIZE-global (8) */
|
||||||
0x29, 0x65, /* USAGE_MAXIMUM (Keyboard Application) */
|
0x81, 0x03, /* INPUT-main (Cnst,Var,Abs) reserved byte */
|
||||||
0x81, 0x00, /* INPUT (Data,Ary,Abs) */
|
|
||||||
0xc0, /* END_COLLECTION */
|
/* 6 key codes for the 104 key keyboard */
|
||||||
|
0x95, 0x06, /* REPORT_COUNT-global (6) keycodes */
|
||||||
|
0x75, 0x08, /* REPORT_SIZE-global (8) bits each (a byte) */
|
||||||
|
0x15, 0x00, /* LOGICAL_MINIMUM-global (0) */
|
||||||
|
0x25, 0xDF, /* LOGICAL_MAXIMUM-global 0xDF (104-key HID codes) */
|
||||||
|
0x05, 0x07, /* USAGE_PAGE-global (Keyboard) */
|
||||||
|
0x19, 0x00, /* USAGE_MINIMUM-local (Reserved/0) no key */
|
||||||
|
0x29, 0xE7, /* USAGE_MAXIMUM-local (Keyboard Right GUI) */
|
||||||
|
0x81, 0x00, /* INPUT-main (Data,Ary,Abs) array of keycodes */
|
||||||
|
|
||||||
|
/* LED report 5 bits for Num Lock through Kana */
|
||||||
|
0x95, 0x05, /* REPORT_COUNT-global (5) 5 LED bits */
|
||||||
|
0x75, 0x01, /* REPORT_SIZE-global (1) each 1 bit */
|
||||||
|
0x05, 0x08, /* USAGE_PAGE-global (LEDs) */
|
||||||
|
0x19, 0x01, /* USAGE_MINIMUM-local (Num Lock) */
|
||||||
|
0x29, 0x05, /* USAGE_MAXIMUM-local (Kana) */
|
||||||
|
0x91, 0x02, /* OUTPUT-main (Data,Var,Abs) bits 0-4 */
|
||||||
|
|
||||||
|
/* 3 bits of padding for the rest of the byte */
|
||||||
|
0x95, 0x01, /* REPORT_COUNT-global (1) one field */
|
||||||
|
0x75, 0x03, /* REPORT_SIZE-global (3) of three bits */
|
||||||
|
0x91, 0x03, /* OUTPUT-main (Cnst,Var,Abs) bit 7 pad */
|
||||||
|
|
||||||
|
0xC0, /* END_COLLECTION */
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
hidReadBufferSize = 8
|
hidReadBufferSize = 8
|
||||||
hidKeyBufferSize = 6
|
HidKeyBufferSize = 6
|
||||||
hidErrorRollOver = 0x01
|
hidErrorRollOver = 0x01
|
||||||
// https://www.usb.org/sites/default/files/documents/hid1_11.pdf
|
// https://www.usb.org/sites/default/files/documents/hid1_11.pdf
|
||||||
// https://www.usb.org/sites/default/files/hut1_2.pdf
|
// https://www.usb.org/sites/default/files/hut1_2.pdf
|
||||||
|
|
@ -74,9 +85,7 @@ const (
|
||||||
KeyboardLedMaskScrollLock = 1 << 2
|
KeyboardLedMaskScrollLock = 1 << 2
|
||||||
KeyboardLedMaskCompose = 1 << 3
|
KeyboardLedMaskCompose = 1 << 3
|
||||||
KeyboardLedMaskKana = 1 << 4
|
KeyboardLedMaskKana = 1 << 4
|
||||||
// power on/off LED is 5
|
ValidKeyboardLedMasks = KeyboardLedMaskNumLock | KeyboardLedMaskCapsLock | KeyboardLedMaskScrollLock | KeyboardLedMaskCompose | KeyboardLedMaskKana
|
||||||
KeyboardLedMaskShift = 1 << 6
|
|
||||||
ValidKeyboardLedMasks = KeyboardLedMaskNumLock | KeyboardLedMaskCapsLock | KeyboardLedMaskScrollLock | KeyboardLedMaskCompose | KeyboardLedMaskKana | KeyboardLedMaskShift
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Synchronization between LED states and CAPS LOCK, NUM LOCK, SCROLL LOCK,
|
// Synchronization between LED states and CAPS LOCK, NUM LOCK, SCROLL LOCK,
|
||||||
|
|
@ -89,7 +98,6 @@ type KeyboardState struct {
|
||||||
ScrollLock bool `json:"scroll_lock"`
|
ScrollLock bool `json:"scroll_lock"`
|
||||||
Compose bool `json:"compose"`
|
Compose bool `json:"compose"`
|
||||||
Kana bool `json:"kana"`
|
Kana bool `json:"kana"`
|
||||||
Shift bool `json:"shift"` // This is not part of the main USB HID spec
|
|
||||||
raw byte
|
raw byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,7 +114,6 @@ func getKeyboardState(b byte) KeyboardState {
|
||||||
ScrollLock: b&KeyboardLedMaskScrollLock != 0,
|
ScrollLock: b&KeyboardLedMaskScrollLock != 0,
|
||||||
Compose: b&KeyboardLedMaskCompose != 0,
|
Compose: b&KeyboardLedMaskCompose != 0,
|
||||||
Kana: b&KeyboardLedMaskKana != 0,
|
Kana: b&KeyboardLedMaskKana != 0,
|
||||||
Shift: b&KeyboardLedMaskShift != 0,
|
|
||||||
raw: b,
|
raw: b,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -153,6 +160,21 @@ func (u *UsbGadget) SetOnKeysDownChange(f func(state KeysDownState)) {
|
||||||
u.onKeysDownChange = &f
|
u.onKeysDownChange = &f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var suspendedKeyDownMessages bool = false
|
||||||
|
var suspendedKeyDownMessagesLock sync.Mutex
|
||||||
|
|
||||||
|
func (u *UsbGadget) SuspendKeyDownMessages() {
|
||||||
|
suspendedKeyDownMessagesLock.Lock()
|
||||||
|
suspendedKeyDownMessages = true
|
||||||
|
suspendedKeyDownMessagesLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) ResumeSuspendKeyDownMessages() {
|
||||||
|
suspendedKeyDownMessagesLock.Lock()
|
||||||
|
suspendedKeyDownMessages = false
|
||||||
|
suspendedKeyDownMessagesLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) SetOnKeepAliveReset(f func()) {
|
func (u *UsbGadget) SetOnKeepAliveReset(f func()) {
|
||||||
u.onKeepAliveReset = &f
|
u.onKeepAliveReset = &f
|
||||||
}
|
}
|
||||||
|
|
@ -169,9 +191,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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -263,12 +285,13 @@ func (u *UsbGadget) listenKeyboardEvents() {
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// reset the counter
|
// reset the suppression counter
|
||||||
u.resetLogSuppressionCounter("keyboardHidFileNil")
|
u.resetLogSuppressionCounter("keyboardHidFileNil")
|
||||||
|
|
||||||
n, err := u.keyboardHidFile.Read(buf)
|
n, err := u.keyboardHidFile.Read(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.logWithSuppression("keyboardHidFileRead", 100, &l, err, "failed to read")
|
u.logWithSuppression("keyboardHidFileRead", 100, &l, err, "failed to read")
|
||||||
|
time.Sleep(100 * time.Millisecond) // Small backoff on read errors to avoid tight looping
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
u.resetLogSuppressionCounter("keyboardHidFileRead")
|
u.resetLogSuppressionCounter("keyboardHidFileRead")
|
||||||
|
|
@ -314,11 +337,12 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := u.writeWithTimeout(u.keyboardHidFile, append([]byte{modifier, 0x00}, keys[:hidKeyBufferSize]...))
|
_, err := u.writeWithTimeout(u.keyboardHidFile, append([]byte{modifier, 0x00}, keys[:HidKeyBufferSize]...))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0")
|
u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0")
|
||||||
u.keyboardHidFile.Close()
|
u.keyboardHidFile.Close()
|
||||||
|
|
@ -353,7 +377,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
|
||||||
|
|
@ -362,11 +386,11 @@ func (u *UsbGadget) UpdateKeysDown(modifier byte, keys []byte) KeysDownState {
|
||||||
func (u *UsbGadget) KeyboardReport(modifier byte, keys []byte) error {
|
func (u *UsbGadget) KeyboardReport(modifier byte, keys []byte) error {
|
||||||
defer u.resetUserInputTime()
|
defer u.resetUserInputTime()
|
||||||
|
|
||||||
if len(keys) > hidKeyBufferSize {
|
if len(keys) > HidKeyBufferSize {
|
||||||
keys = keys[:hidKeyBufferSize]
|
keys = keys[:HidKeyBufferSize]
|
||||||
}
|
}
|
||||||
if len(keys) < hidKeyBufferSize {
|
if len(keys) < HidKeyBufferSize {
|
||||||
keys = append(keys, make([]byte, hidKeyBufferSize-len(keys))...)
|
keys = append(keys, make([]byte, HidKeyBufferSize-len(keys))...)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := u.keyboardWriteHidFile(modifier, keys)
|
err := u.keyboardWriteHidFile(modifier, keys)
|
||||||
|
|
@ -449,7 +473,7 @@ func (u *UsbGadget) keypressReport(key byte, press bool) (KeysDownState, error)
|
||||||
// handle other keys that are not modifier keys by placing or removing them
|
// handle other keys that are not modifier keys by placing or removing them
|
||||||
// from the key buffer since the buffer tracks currently pressed keys
|
// from the key buffer since the buffer tracks currently pressed keys
|
||||||
overrun := true
|
overrun := true
|
||||||
for i := range hidKeyBufferSize {
|
for i := range HidKeyBufferSize {
|
||||||
// If we find the key in the buffer the buffer, we either remove it (if press is false)
|
// If we find the key in the buffer the buffer, we either remove it (if press is false)
|
||||||
// or do nothing (if down is true) because the buffer tracks currently pressed keys
|
// or do nothing (if down is true) because the buffer tracks currently pressed keys
|
||||||
// and if we find a zero byte, we can place the key there (if press is true)
|
// and if we find a zero byte, we can place the key there (if press is true)
|
||||||
|
|
@ -460,7 +484,7 @@ func (u *UsbGadget) keypressReport(key byte, press bool) (KeysDownState, error)
|
||||||
// we are releasing the key, remove it from the buffer
|
// we are releasing the key, remove it from the buffer
|
||||||
if keys[i] != 0 {
|
if keys[i] != 0 {
|
||||||
copy(keys[i:], keys[i+1:])
|
copy(keys[i:], keys[i+1:])
|
||||||
keys[hidKeyBufferSize-1] = 0 // Clear the last byte
|
keys[HidKeyBufferSize-1] = 0 // Clear the last byte
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
overrun = false // We found a slot for the key
|
overrun = false // We found a slot for the key
|
||||||
|
|
@ -484,6 +508,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev
|
||||||
keyboardStateCtx: keyboardCtx,
|
keyboardStateCtx: keyboardCtx,
|
||||||
keyboardStateCancel: keyboardCancel,
|
keyboardStateCancel: keyboardCancel,
|
||||||
keyboardState: 0,
|
keyboardState: 0,
|
||||||
keysDownState: KeysDownState{Modifier: 0, Keys: []byte{0, 0, 0, 0, 0, 0}}, // must be initialized to hidKeyBufferSize (6) zero bytes
|
keysDownState: KeysDownState{Modifier: 0, Keys: []byte{0, 0, 0, 0, 0, 0}}, // must be initialized to usbgadget.HidKeyBufferSize (6) zero bytes
|
||||||
kbdAutoReleaseTimers: make(map[byte]*time.Timer),
|
kbdAutoReleaseTimers: make(map[byte]*time.Timer),
|
||||||
enabledDevices: *enabledDevices,
|
enabledDevices: *enabledDevices,
|
||||||
lastUserInput: time.Now(),
|
lastUserInput: time.Now(),
|
||||||
|
|
|
||||||
153
jsonrpc.go
153
jsonrpc.go
|
|
@ -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"
|
||||||
|
|
@ -1063,91 +1063,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, usbgadget.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")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,6 @@
|
||||||
"info_relayed_by_cloudflare": "Videresendt af Cloudflare",
|
"info_relayed_by_cloudflare": "Videresendt af Cloudflare",
|
||||||
"info_resolution": "Opløsning:",
|
"info_resolution": "Opløsning:",
|
||||||
"info_scroll_lock": "Scroll Lock",
|
"info_scroll_lock": "Scroll Lock",
|
||||||
"info_shift": "Flytte",
|
|
||||||
"info_usb_state": "USB-tilstand:",
|
"info_usb_state": "USB-tilstand:",
|
||||||
"info_video_size": "Videostørrelse:",
|
"info_video_size": "Videostørrelse:",
|
||||||
"input_disabled": "Input deaktiveret",
|
"input_disabled": "Input deaktiveret",
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,6 @@
|
||||||
"info_relayed_by_cloudflare": "Weitergeleitet von Cloudflare",
|
"info_relayed_by_cloudflare": "Weitergeleitet von Cloudflare",
|
||||||
"info_resolution": "Auflösung:",
|
"info_resolution": "Auflösung:",
|
||||||
"info_scroll_lock": "Rollen-Taste",
|
"info_scroll_lock": "Rollen-Taste",
|
||||||
"info_shift": "Schicht",
|
|
||||||
"info_usb_state": "USB-Status:",
|
"info_usb_state": "USB-Status:",
|
||||||
"info_video_size": "Videogröße:",
|
"info_video_size": "Videogröße:",
|
||||||
"input_disabled": "Eingabe deaktiviert",
|
"input_disabled": "Eingabe deaktiviert",
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,6 @@
|
||||||
"info_relayed_by_cloudflare": "Relayed by Cloudflare",
|
"info_relayed_by_cloudflare": "Relayed by Cloudflare",
|
||||||
"info_resolution": "Resolution:",
|
"info_resolution": "Resolution:",
|
||||||
"info_scroll_lock": "Scroll Lock",
|
"info_scroll_lock": "Scroll Lock",
|
||||||
"info_shift": "Shift",
|
|
||||||
"info_usb_state": "USB State:",
|
"info_usb_state": "USB State:",
|
||||||
"info_video_size": "Video Size:",
|
"info_video_size": "Video Size:",
|
||||||
"input_disabled": "Input disabled",
|
"input_disabled": "Input disabled",
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,6 @@
|
||||||
"info_relayed_by_cloudflare": "Retransmitido por Cloudflare",
|
"info_relayed_by_cloudflare": "Retransmitido por Cloudflare",
|
||||||
"info_resolution": "Resolución:",
|
"info_resolution": "Resolución:",
|
||||||
"info_scroll_lock": "Bloq Despl",
|
"info_scroll_lock": "Bloq Despl",
|
||||||
"info_shift": "Cambio",
|
|
||||||
"info_usb_state": "Estado USB:",
|
"info_usb_state": "Estado USB:",
|
||||||
"info_video_size": "Tamaño del vídeo:",
|
"info_video_size": "Tamaño del vídeo:",
|
||||||
"input_disabled": "Entrada deshabilitada",
|
"input_disabled": "Entrada deshabilitada",
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,6 @@
|
||||||
"info_relayed_by_cloudflare": "Relayé par Cloudflare",
|
"info_relayed_by_cloudflare": "Relayé par Cloudflare",
|
||||||
"info_resolution": "Résolution :",
|
"info_resolution": "Résolution :",
|
||||||
"info_scroll_lock": "Verrouillage du défilement",
|
"info_scroll_lock": "Verrouillage du défilement",
|
||||||
"info_shift": "Maj",
|
|
||||||
"info_usb_state": "État USB :",
|
"info_usb_state": "État USB :",
|
||||||
"info_video_size": "Taille de la vidéo :",
|
"info_video_size": "Taille de la vidéo :",
|
||||||
"input_disabled": "Entrée désactivée",
|
"input_disabled": "Entrée désactivée",
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,6 @@
|
||||||
"info_relayed_by_cloudflare": "Rilasciato da Cloudflare",
|
"info_relayed_by_cloudflare": "Rilasciato da Cloudflare",
|
||||||
"info_resolution": "Risoluzione:",
|
"info_resolution": "Risoluzione:",
|
||||||
"info_scroll_lock": "Blocco scorrimento",
|
"info_scroll_lock": "Blocco scorrimento",
|
||||||
"info_shift": "Spostare",
|
|
||||||
"info_usb_state": "Stato USB:",
|
"info_usb_state": "Stato USB:",
|
||||||
"info_video_size": "Dimensioni video:",
|
"info_video_size": "Dimensioni video:",
|
||||||
"input_disabled": "Input disabilitato",
|
"input_disabled": "Input disabilitato",
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,6 @@
|
||||||
"info_relayed_by_cloudflare": "Videresendt av Cloudflare",
|
"info_relayed_by_cloudflare": "Videresendt av Cloudflare",
|
||||||
"info_resolution": "Oppløsning:",
|
"info_resolution": "Oppløsning:",
|
||||||
"info_scroll_lock": "Scroll Lock",
|
"info_scroll_lock": "Scroll Lock",
|
||||||
"info_shift": "Skifte",
|
|
||||||
"info_usb_state": "USB-tilstand:",
|
"info_usb_state": "USB-tilstand:",
|
||||||
"info_video_size": "Videostørrelse:",
|
"info_video_size": "Videostørrelse:",
|
||||||
"input_disabled": "Inndata deaktivert",
|
"input_disabled": "Inndata deaktivert",
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,6 @@
|
||||||
"info_relayed_by_cloudflare": "Vidarebefordras av Cloudflare",
|
"info_relayed_by_cloudflare": "Vidarebefordras av Cloudflare",
|
||||||
"info_resolution": "Upplösning:",
|
"info_resolution": "Upplösning:",
|
||||||
"info_scroll_lock": "Scroll Lock",
|
"info_scroll_lock": "Scroll Lock",
|
||||||
"info_shift": "Flytta",
|
|
||||||
"info_usb_state": "USB-status:",
|
"info_usb_state": "USB-status:",
|
||||||
"info_video_size": "Videostorlek:",
|
"info_video_size": "Videostorlek:",
|
||||||
"input_disabled": "Inmatning inaktiverad",
|
"input_disabled": "Inmatning inaktiverad",
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,6 @@
|
||||||
"info_relayed_by_cloudflare": "由 Cloudflare 转发",
|
"info_relayed_by_cloudflare": "由 Cloudflare 转发",
|
||||||
"info_resolution": "分辨率:",
|
"info_resolution": "分辨率:",
|
||||||
"info_scroll_lock": "滚动锁定",
|
"info_scroll_lock": "滚动锁定",
|
||||||
"info_shift": "Shift",
|
|
||||||
"info_usb_state": "USB 状态:",
|
"info_usb_state": "USB 状态:",
|
||||||
"info_video_size": "视频大小:",
|
"info_video_size": "视频大小:",
|
||||||
"input_disabled": "输入禁用",
|
"input_disabled": "输入禁用",
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "kvm-ui",
|
"name": "kvm-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2025.10.24.2140",
|
"version": "2025.11.05.2130",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^22.20.0"
|
"node": "^22.20.0"
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
"@xterm/addon-webgl": "^0.18.0",
|
"@xterm/addon-webgl": "^0.18.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"cva": "^1.0.0-beta.4",
|
"cva": "^1.0.0-beta.4",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.19",
|
||||||
"eslint-import-resolver-alias": "^1.1.2",
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
"focus-trap-react": "^11.0.4",
|
"focus-trap-react": "^11.0.4",
|
||||||
"framer-motion": "^12.23.24",
|
"framer-motion": "^12.23.24",
|
||||||
|
|
@ -47,23 +47,24 @@
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-animate-height": "^3.2.3",
|
"react-animate-height": "^3.2.3",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-hook-form": "^7.65.0",
|
"react-hook-form": "^7.66.0",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-router": "^7.9.5",
|
"react-router": "^7.9.5",
|
||||||
"react-simple-keyboard": "^3.8.131",
|
"react-simple-keyboard": "^3.8.132",
|
||||||
"react-use-websocket": "^4.13.0",
|
"react-use-websocket": "^4.13.0",
|
||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.10",
|
||||||
"recharts": "^3.3.0",
|
"recharts": "^3.3.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
"validator": "^13.15.15",
|
"uuid": "^13.0.0",
|
||||||
|
"validator": "^13.15.20",
|
||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.4.1",
|
"@eslint/compat": "^1.4.1",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "^9.39.0",
|
"@eslint/js": "^9.39.1",
|
||||||
"@inlang/cli": "^3.0.12",
|
"@inlang/cli": "^3.0.12",
|
||||||
"@inlang/paraglide-js": "^2.4.0",
|
"@inlang/paraglide-js": "^2.4.0",
|
||||||
"@inlang/plugin-m-function-matcher": "^2.1.0",
|
"@inlang/plugin-m-function-matcher": "^2.1.0",
|
||||||
|
|
@ -76,25 +77,25 @@
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"@types/validator": "^13.15.3",
|
"@types/validator": "^13.15.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
"@typescript-eslint/eslint-plugin": "^8.46.3",
|
||||||
"@typescript-eslint/parser": "^8.46.2",
|
"@typescript-eslint/parser": "^8.46.3",
|
||||||
"@vitejs/plugin-react-swc": "^4.2.0",
|
"@vitejs/plugin-react-swc": "^4.2.1",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"eslint": "^9.38.0",
|
"eslint": "^9.39.1",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-prettier": "^5.5.4",
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"globals": "^16.4.0",
|
"globals": "^16.5.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.1",
|
"prettier-plugin-tailwindcss": "^0.7.1",
|
||||||
"tailwindcss": "^4.1.16",
|
"tailwindcss": "^4.1.16",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.1.12",
|
"vite": "^7.2.0",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -166,10 +166,6 @@ export default function InfoBar() {
|
||||||
{keyboardLedState.kana ? (
|
{keyboardLedState.kana ? (
|
||||||
<div className="shrink-0 p-1 px-1.5 text-xs">{m.info_kana()}</div>
|
<div className="shrink-0 p-1 px-1.5 text-xs">{m.info_kana()}</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{keyboardLedState.shift ? (
|
|
||||||
<div className="shrink-0 p-1 px-1.5 text-xs">{m.info_shift()}</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ import { TextAreaWithLabel } from "@components/TextArea";
|
||||||
// uint32 max value / 4
|
// uint32 max value / 4
|
||||||
const pasteMaxLength = 1073741824;
|
const pasteMaxLength = 1073741824;
|
||||||
const defaultDelay = 20;
|
const defaultDelay = 20;
|
||||||
|
const minimumDelay = 5;
|
||||||
|
const maximumDelay = 65534;
|
||||||
|
|
||||||
export default function PasteModal() {
|
export default function PasteModal() {
|
||||||
const TextAreaRef = useRef<HTMLTextAreaElement>(null);
|
const TextAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
@ -31,7 +33,7 @@ export default function PasteModal() {
|
||||||
const [invalidChars, setInvalidChars] = useState<string[]>([]);
|
const [invalidChars, setInvalidChars] = useState<string[]>([]);
|
||||||
const [delayValue, setDelayValue] = useState(defaultDelay);
|
const [delayValue, setDelayValue] = useState(defaultDelay);
|
||||||
const delay = useMemo(() => {
|
const delay = useMemo(() => {
|
||||||
if (delayValue < 0 || delayValue > 65534) {
|
if (delayValue < minimumDelay || delayValue > maximumDelay) {
|
||||||
return defaultDelay;
|
return defaultDelay;
|
||||||
}
|
}
|
||||||
return delayValue;
|
return delayValue;
|
||||||
|
|
@ -52,10 +54,12 @@ export default function PasteModal() {
|
||||||
}, [send, setKeyboardLayout]);
|
}, [send, setKeyboardLayout]);
|
||||||
|
|
||||||
const onCancelPasteMode = useCallback(() => {
|
const onCancelPasteMode = useCallback(() => {
|
||||||
cancelExecuteMacro();
|
if (isPasteInProgress) {
|
||||||
|
cancelExecuteMacro();
|
||||||
|
}
|
||||||
setDisableVideoFocusTrap(false);
|
setDisableVideoFocusTrap(false);
|
||||||
setInvalidChars([]);
|
setInvalidChars([]);
|
||||||
}, [setDisableVideoFocusTrap, cancelExecuteMacro]);
|
}, [isPasteInProgress, setDisableVideoFocusTrap, cancelExecuteMacro]);
|
||||||
|
|
||||||
const onConfirmPaste = useCallback(async () => {
|
const onConfirmPaste = useCallback(async () => {
|
||||||
if (!TextAreaRef.current || !selectedKeyboard) return;
|
if (!TextAreaRef.current || !selectedKeyboard) return;
|
||||||
|
|
@ -189,18 +193,18 @@ export default function PasteModal() {
|
||||||
type="number"
|
type="number"
|
||||||
label={m.paste_modal_delay_between_keys()}
|
label={m.paste_modal_delay_between_keys()}
|
||||||
placeholder={m.paste_modal_delay_between_keys()}
|
placeholder={m.paste_modal_delay_between_keys()}
|
||||||
min={50}
|
min={minimumDelay}
|
||||||
max={65534}
|
max={maximumDelay}
|
||||||
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 < minimumDelay || delayValue > maximumDelay) && (
|
||||||
<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">
|
||||||
{m.paste_modal_delay_out_of_range({ min: 50, max: 65534 })}
|
{m.paste_modal_delay_out_of_range({ min: minimumDelay, max: maximumDelay })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -224,7 +228,7 @@ export default function PasteModal() {
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="blank"
|
theme="blank"
|
||||||
text={m.cancel()}
|
text={isPasteInProgress ? m.cancel() : m.close()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onCancelPasteMode();
|
onCancelPasteMode();
|
||||||
close();
|
close();
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
@ -300,7 +303,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}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -379,13 +382,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 === "")
|
||||||
|
? "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)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -431,6 +451,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 => {
|
||||||
|
|
|
||||||
|
|
@ -473,7 +473,6 @@ export interface KeyboardLedState {
|
||||||
scroll_lock: boolean;
|
scroll_lock: boolean;
|
||||||
compose: boolean;
|
compose: boolean;
|
||||||
kana: boolean;
|
kana: boolean;
|
||||||
shift: boolean; // Optional, as not all keyboards have a shift LED
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hidKeyBufferSize = 6;
|
export const hidKeyBufferSize = 6;
|
||||||
|
|
@ -509,7 +508,7 @@ export interface HidState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useHidStore = create<HidState>(set => ({
|
export const useHidStore = create<HidState>(set => ({
|
||||||
keyboardLedState: { num_lock: false, caps_lock: false, scroll_lock: false, compose: false, kana: false, shift: false } as KeyboardLedState,
|
keyboardLedState: { num_lock: false, caps_lock: false, scroll_lock: false, compose: false, kana: false } as KeyboardLedState,
|
||||||
setKeyboardLedState: (ledState: KeyboardLedState): void => set({ keyboardLedState: ledState }),
|
setKeyboardLedState: (ledState: KeyboardLedState): void => set({ keyboardLedState: ledState }),
|
||||||
|
|
||||||
keysDownState: { modifier: 0, keys: [0, 0, 0, 0, 0, 0] } as KeysDownState,
|
keysDownState: { modifier: 0, keys: [0, 0, 0, 0, 0, 0] } as KeysDownState,
|
||||||
|
|
|
||||||
|
|
@ -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],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -287,13 +287,11 @@ export default function useKeyboard() {
|
||||||
async (steps: MacroSteps) => {
|
async (steps: MacroSteps) => {
|
||||||
const macro: KeyboardMacroStep[] = [];
|
const macro: KeyboardMacroStep[] = [];
|
||||||
|
|
||||||
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
|
||||||
if (keyValues.length > 0 || modifierMask > 0) {
|
if (keyValues.length > 0 || modifierMask > 0) {
|
||||||
|
|
@ -302,10 +300,9 @@ export default function useKeyboard() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendKeyboardMacroEventHidRpc(macro);
|
sendKeyboardMacroEventHidRpc(macro);
|
||||||
},
|
}, [sendKeyboardMacroEventHidRpc]);
|
||||||
[sendKeyboardMacroEventHidRpc],
|
|
||||||
);
|
|
||||||
|
|
||||||
const executeMacroClientSide = useCallback(
|
const executeMacroClientSide = useCallback(
|
||||||
async (steps: MacroSteps) => {
|
async (steps: MacroSteps) => {
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@
|
||||||
// [Universal Serial Bus HID Usage Tables: Section 10](https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf)
|
// [Universal Serial Bus HID Usage Tables: Section 10](https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf)
|
||||||
// These are all the key codes (not scan codes) that an 85/101/102 keyboard might have on it
|
// These are all the key codes (not scan codes) that an 85/101/102 keyboard might have on it
|
||||||
export const keys = {
|
export const keys = {
|
||||||
Again: 0x79,
|
Again: 0x79, // aka Clear
|
||||||
AlternateErase: 0x9d,
|
AlternateErase: 0x9d,
|
||||||
AltGr: 0xe6, // aka AltRight
|
AltGr: 0xe6, // aka AltRight
|
||||||
AltLeft: 0xe2,
|
AltLeft: 0xe2,
|
||||||
AltRight: 0xe6,
|
AltRight: 0xe6,
|
||||||
Application: 0x65,
|
Application: 0x65, // aka ContextMenu
|
||||||
ArrowDown: 0x51,
|
ArrowDown: 0x51,
|
||||||
ArrowLeft: 0x50,
|
ArrowLeft: 0x50,
|
||||||
ArrowRight: 0x4f,
|
ArrowRight: 0x4f,
|
||||||
|
|
@ -25,11 +25,10 @@ export const keys = {
|
||||||
ClearAgain: 0xa2,
|
ClearAgain: 0xa2,
|
||||||
Comma: 0x36,
|
Comma: 0x36,
|
||||||
Compose: 0xe3,
|
Compose: 0xe3,
|
||||||
ContextMenu: 0x65,
|
|
||||||
ControlLeft: 0xe0,
|
ControlLeft: 0xe0,
|
||||||
ControlRight: 0xe4,
|
ControlRight: 0xe4,
|
||||||
Copy: 0x7c,
|
Copy: 0x7c,
|
||||||
CrSel: 0xa3,
|
CrSel: 0xa3, // aka Props
|
||||||
CurrencySubunit: 0xb5,
|
CurrencySubunit: 0xb5,
|
||||||
CurrencyUnit: 0xb4,
|
CurrencyUnit: 0xb4,
|
||||||
Cut: 0x7b,
|
Cut: 0x7b,
|
||||||
|
|
@ -49,7 +48,7 @@ export const keys = {
|
||||||
Enter: 0x28,
|
Enter: 0x28,
|
||||||
Equal: 0x2e,
|
Equal: 0x2e,
|
||||||
Escape: 0x29,
|
Escape: 0x29,
|
||||||
Execute: 0x74,
|
Execute: 0x74, // aka Open
|
||||||
ExSel: 0xa4,
|
ExSel: 0xa4,
|
||||||
F1: 0x3a,
|
F1: 0x3a,
|
||||||
F2: 0x3b,
|
F2: 0x3b,
|
||||||
|
|
@ -77,14 +76,14 @@ export const keys = {
|
||||||
F24: 0x73,
|
F24: 0x73,
|
||||||
Find: 0x7e,
|
Find: 0x7e,
|
||||||
Grave: 0x35,
|
Grave: 0x35,
|
||||||
HashTilde: 0x32, // non-US # and ~
|
HashTilde: 0x32, // non-US # and ~ (typically near Enter key)
|
||||||
Help: 0x75,
|
Help: 0x75,
|
||||||
Home: 0x4a,
|
Home: 0x4a,
|
||||||
Insert: 0x49,
|
Insert: 0x49,
|
||||||
International7: 0x8d,
|
International7: 0x8d,
|
||||||
International8: 0x8e,
|
International8: 0x8e,
|
||||||
International9: 0x8f,
|
International9: 0x8f,
|
||||||
IntlBackslash: 0x64, // non-US \ and |
|
IntlBackslash: 0x64, // non-US \ and | (typically near Left Shift key)
|
||||||
KeyA: 0x04,
|
KeyA: 0x04,
|
||||||
KeyB: 0x05,
|
KeyB: 0x05,
|
||||||
KeyC: 0x06,
|
KeyC: 0x06,
|
||||||
|
|
@ -111,17 +110,17 @@ export const keys = {
|
||||||
KeyX: 0x1b,
|
KeyX: 0x1b,
|
||||||
KeyY: 0x1c,
|
KeyY: 0x1c,
|
||||||
KeyZ: 0x1d,
|
KeyZ: 0x1d,
|
||||||
KeyRO: 0x87,
|
RO: 0x87, // aka International1
|
||||||
KatakanaHiragana: 0x88,
|
KatakanaHiragana: 0x88, // aka International2
|
||||||
Yen: 0x89,
|
Yen: 0x89, // aka International3
|
||||||
Henkan: 0x8a,
|
Henkan: 0x8a, // aka International4
|
||||||
Muhenkan: 0x8b,
|
Muhenkan: 0x8b, // aka International5
|
||||||
KPJPComma: 0x8c,
|
KPJPComma: 0x8c, // aka International6
|
||||||
Hangeul: 0x90,
|
Hangeul: 0x90, // aka Lang1
|
||||||
Hanja: 0x91,
|
Hanja: 0x91, // aka Lang2
|
||||||
Katakana: 0x92,
|
Katakana: 0x92, // aka Lang3
|
||||||
Hiragana: 0x93,
|
Hiragana: 0x93, // aka Lang4
|
||||||
ZenkakuHankaku: 0x94,
|
ZenkakuHankaku: 0x94, // aka Lang5
|
||||||
LockingCapsLock: 0x82,
|
LockingCapsLock: 0x82,
|
||||||
LockingNumLock: 0x83,
|
LockingNumLock: 0x83,
|
||||||
LockingScrollLock: 0x84,
|
LockingScrollLock: 0x84,
|
||||||
|
|
@ -129,9 +128,29 @@ export const keys = {
|
||||||
Lang7: 0x96,
|
Lang7: 0x96,
|
||||||
Lang8: 0x97,
|
Lang8: 0x97,
|
||||||
Lang9: 0x98,
|
Lang9: 0x98,
|
||||||
|
MediaBack: 0xF1,
|
||||||
|
MediaCalc: 0xFB,
|
||||||
|
MediaCoffee: 0xF9,
|
||||||
|
MediaEdit: 0xF7,
|
||||||
|
MediaEjectCD: 0xEC,
|
||||||
|
MediaFind: 0xF4,
|
||||||
|
MediaForward: 0xF2,
|
||||||
|
MediaMute: 0xEF,
|
||||||
|
MediaNextSong: 0xEB,
|
||||||
|
MediaPlayPause: 0xE8,
|
||||||
|
MediaPreviousSong: 0xEA,
|
||||||
|
MediaRefresh: 0xFA,
|
||||||
|
MediaScrollDown: 0xF6,
|
||||||
|
MediaScrollUp: 0xF5,
|
||||||
|
MediaSleep: 0xF8,
|
||||||
|
MediaStop: 0xF3,
|
||||||
|
MediaStopCD: 0xE9,
|
||||||
|
MediaVolumeDown: 0xEE,
|
||||||
|
MediaVolumeUp: 0xED,
|
||||||
|
MediaWWW: 0xF0,
|
||||||
Menu: 0x76,
|
Menu: 0x76,
|
||||||
MetaLeft: 0xe3,
|
MetaLeft: 0xe3, // aka LeftGUI
|
||||||
MetaRight: 0xe7,
|
MetaRight: 0xe7, // aka RightGUI
|
||||||
Minus: 0x2d,
|
Minus: 0x2d,
|
||||||
Mute: 0x7f,
|
Mute: 0x7f,
|
||||||
NumLock: 0x53, // and Clear
|
NumLock: 0x53, // and Clear
|
||||||
|
|
@ -157,9 +176,8 @@ export const keys = {
|
||||||
NumpadClearEntry: 0xd9,
|
NumpadClearEntry: 0xd9,
|
||||||
NumpadColon: 0xcb,
|
NumpadColon: 0xcb,
|
||||||
NumpadComma: 0x85,
|
NumpadComma: 0x85,
|
||||||
NumpadDecimal: 0x63, // and Delete
|
NumpadDecimal: 0x63, // and NumpadDelete
|
||||||
NumpadDecimalBase: 0xdc,
|
NumpadDecimalBase: 0xdc,
|
||||||
NumpadDelete: 0x63,
|
|
||||||
NumpadDivide: 0x54,
|
NumpadDivide: 0x54,
|
||||||
NumpadDownArrow: 0x5a,
|
NumpadDownArrow: 0x5a,
|
||||||
NumpadEnd: 0x59,
|
NumpadEnd: 0x59,
|
||||||
|
|
@ -211,14 +229,14 @@ export const keys = {
|
||||||
PageUp: 0x4b,
|
PageUp: 0x4b,
|
||||||
Paste: 0x7d,
|
Paste: 0x7d,
|
||||||
Pause: 0x48,
|
Pause: 0x48,
|
||||||
Period: 0x37, // aka Dot
|
Period: 0x37, // aka Dot
|
||||||
Power: 0x66,
|
Power: 0x66,
|
||||||
PrintScreen: 0x46,
|
PrintScreen: 0x46, // aka SysRq
|
||||||
Prior: 0x9d,
|
Prior: 0x9d,
|
||||||
Quote: 0x34, // aka Single Quote or Apostrophe
|
Quote: 0x34, // aka Single Quote or Apostrophe
|
||||||
Return: 0x9e,
|
Return: 0x9e,
|
||||||
ScrollLock: 0x47,
|
ScrollLock: 0x47, // aka ScrLk
|
||||||
Select: 0x77,
|
Select: 0x77, // aka Front
|
||||||
Semicolon: 0x33,
|
Semicolon: 0x33,
|
||||||
Separator: 0x9f,
|
Separator: 0x9f,
|
||||||
ShiftLeft: 0xe1,
|
ShiftLeft: 0xe1,
|
||||||
|
|
@ -240,7 +258,7 @@ export const deadKeys = {
|
||||||
Breve: 0x02d8,
|
Breve: 0x02d8,
|
||||||
Caron: 0x02c7,
|
Caron: 0x02c7,
|
||||||
Cedilla: 0x00b8,
|
Cedilla: 0x00b8,
|
||||||
Circumflex: 0x005e, // or 0x02c6?
|
Circumflex: 0x02c6,
|
||||||
Comma: 0x002c,
|
Comma: 0x002c,
|
||||||
Dot: 0x00b7,
|
Dot: 0x00b7,
|
||||||
DoubleAcute: 0x02dd,
|
DoubleAcute: 0x02dd,
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ def main(argv):
|
||||||
)
|
)
|
||||||
|
|
||||||
report = {
|
report = {
|
||||||
"generated_at": datetime.utcnow().isoformat() + "Z",
|
"generated_at": datetime.now().isoformat(),
|
||||||
"en_json": str(en_path),
|
"en_json": str(en_path),
|
||||||
"total_string_keys": total_keys,
|
"total_string_keys": total_keys,
|
||||||
"duplicate_groups": sorted(
|
"duplicate_groups": sorted(
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ def main():
|
||||||
|
|
||||||
print(f"Generating report for {len(usages)} usages ...")
|
print(f"Generating report for {len(usages)} usages ...")
|
||||||
report = {
|
report = {
|
||||||
"generated_at": datetime.utcnow().isoformat() + "Z",
|
"generated_at": datetime.now().isoformat(),
|
||||||
"en_json": str(en_path),
|
"en_json": str(en_path),
|
||||||
"src_root": args.src,
|
"src_root": args.src,
|
||||||
"total_keys": len(keys),
|
"total_keys": len(keys),
|
||||||
|
|
|
||||||
2
web.go
2
web.go
|
|
@ -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})
|
||||||
|
|
|
||||||
56
webrtc.go
56
webrtc.go
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -76,7 +76,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 {
|
||||||
|
|
@ -121,19 +122,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -188,17 +190,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")
|
||||||
|
|
@ -248,7 +251,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() {
|
||||||
|
|
@ -258,8 +261,8 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for i := 0; i < len(session.hidQueue); i++ {
|
for queueIndex := range session.hidQueues {
|
||||||
go session.handleQueues(i)
|
go session.handleQueue(session.hidQueues[queueIndex])
|
||||||
}
|
}
|
||||||
|
|
||||||
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
|
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
|
||||||
|
|
@ -284,7 +287,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()
|
||||||
|
|
@ -352,22 +359,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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue