mirror of https://github.com/jetkvm/kvm.git
Compare commits
8 Commits
761689f216
...
bf8ee5938e
| Author | SHA1 | Date |
|---|---|---|
|
|
bf8ee5938e | |
|
|
20a23de227 | |
|
|
80a8b9e9e3 | |
|
|
4e5ae37bf4 | |
|
|
421666ccdc | |
|
|
11b3e8935f | |
|
|
9593a81cb8 | |
|
|
5cac4c4604 |
20
hidrpc.go
20
hidrpc.go
|
|
@ -25,11 +25,7 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
|
|||
}
|
||||
session.hidRPCAvailable = true
|
||||
case hidrpc.TypeKeypressReport, hidrpc.TypeKeyboardReport:
|
||||
keysDownState, err := handleHidRPCKeyboardInput(message)
|
||||
if keysDownState != nil {
|
||||
session.reportHidRPCKeysDownState(*keysDownState)
|
||||
}
|
||||
rpcErr = err
|
||||
rpcErr = handleHidRPCKeyboardInput(message)
|
||||
case hidrpc.TypeKeypressKeepAliveReport:
|
||||
gadget.DelayAutoRelease()
|
||||
case hidrpc.TypePointerReport:
|
||||
|
|
@ -95,27 +91,25 @@ func onHidMessage(msg hidQueueMessage, session *Session) {
|
|||
}
|
||||
}
|
||||
|
||||
func handleHidRPCKeyboardInput(message hidrpc.Message) (*usbgadget.KeysDownState, error) {
|
||||
func handleHidRPCKeyboardInput(message hidrpc.Message) error {
|
||||
switch message.Type() {
|
||||
case hidrpc.TypeKeypressReport:
|
||||
keypressReport, err := message.KeypressReport()
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to get keypress report")
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
keysDownState, rpcError := rpcKeypressReport(keypressReport.Key, keypressReport.Press)
|
||||
return &keysDownState, rpcError
|
||||
return rpcKeypressReport(keypressReport.Key, keypressReport.Press)
|
||||
case hidrpc.TypeKeyboardReport:
|
||||
keyboardReport, err := message.KeyboardReport()
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to get keyboard report")
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
keysDownState, rpcError := rpcKeyboardReport(keyboardReport.Modifier, keyboardReport.Keys)
|
||||
return &keysDownState, rpcError
|
||||
return rpcKeyboardReport(keyboardReport.Modifier, keyboardReport.Keys)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown HID RPC message type: %d", message.Type())
|
||||
return fmt.Errorf("unknown HID RPC message type: %d", message.Type())
|
||||
}
|
||||
|
||||
func reportHidRPC(params any, session *Session) {
|
||||
|
|
|
|||
|
|
@ -99,11 +99,3 @@ func NewKeydownStateMessage(state usbgadget.KeysDownState) *Message {
|
|||
d: data,
|
||||
}
|
||||
}
|
||||
|
||||
// NewKeypressKeepAliveMessage creates a new keypress keep alive message.
|
||||
func NewKeypressKeepAliveMessage() *Message {
|
||||
return &Message{
|
||||
t: TypeKeypressKeepAliveReport,
|
||||
d: []byte{},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,13 +56,12 @@ type NetworkConfig struct {
|
|||
}
|
||||
|
||||
func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {
|
||||
mode := c.MDNSMode.String
|
||||
listenOptions := &mdns.MDNSListenOptions{
|
||||
IPv4: true,
|
||||
IPv6: true,
|
||||
IPv4: c.IPv4Mode.String != "disabled",
|
||||
IPv6: c.IPv6Mode.String != "disabled",
|
||||
}
|
||||
|
||||
switch mode {
|
||||
switch c.MDNSMode.String {
|
||||
case "ipv4_only":
|
||||
listenOptions.IPv6 = false
|
||||
case "ipv6_only":
|
||||
|
|
|
|||
|
|
@ -239,6 +239,10 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
|||
ipv4Addresses = append(ipv4Addresses, addr.IP)
|
||||
ipv4AddressesString = append(ipv4AddressesString, addr.IPNet.String())
|
||||
} else if addr.IP.To16() != nil {
|
||||
if s.config.IPv6Mode.String == "disabled" {
|
||||
continue
|
||||
}
|
||||
|
||||
scopedLogger := s.l.With().Str("ipv6", addr.IP.String()).Logger()
|
||||
// check if it's a link local address
|
||||
if addr.IP.IsLinkLocalUnicast() {
|
||||
|
|
@ -287,6 +291,7 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
|||
}
|
||||
s.ipv4Addresses = ipv4AddressesString
|
||||
|
||||
if s.config.IPv6Mode.String != "disabled" {
|
||||
if ipv6LinkLocal != nil {
|
||||
if s.ipv6LinkLocal == nil || s.ipv6LinkLocal.String() != ipv6LinkLocal.String() {
|
||||
scopedLogger := s.l.With().Str("ipv6", ipv6LinkLocal.String()).Logger()
|
||||
|
|
@ -318,6 +323,7 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
|||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if it's the initial check, we'll set changed to false
|
||||
initialCheck := !s.checked
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ func (s *NetworkInterfaceState) IPv6LinkLocalAddress() string {
|
|||
func (s *NetworkInterfaceState) RpcGetNetworkState() RpcNetworkState {
|
||||
ipv6Addresses := make([]RpcIPv6Address, 0)
|
||||
|
||||
if s.ipv6Addresses != nil {
|
||||
if s.ipv6Addresses != nil && s.config.IPv6Mode.String != "disabled" {
|
||||
for _, addr := range s.ipv6Addresses {
|
||||
ipv6Addresses = append(ipv6Addresses, RpcIPv6Address{
|
||||
Address: addr.Prefix.String(),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/xid"
|
||||
|
|
@ -27,7 +28,7 @@ var keyboardConfig = gadgetConfigItem{
|
|||
|
||||
// macOS default: 15 * 15 = 225ms https://discussions.apple.com/thread/1316947?sortBy=rank
|
||||
// Linux default: 250ms https://man.archlinux.org/man/kbdrate.8.en
|
||||
// Windows default: 1ms `HKEY_CURRENT_USER\Control Panel\Accessibility\Keyboard Response\AutoRepeatDelay`
|
||||
// Windows default: 1s `HKEY_CURRENT_USER\Control Panel\Accessibility\Keyboard Response\AutoRepeatDelay`
|
||||
const autoReleaseKeyboardInterval = time.Millisecond * 100
|
||||
|
||||
// Source: https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt
|
||||
|
|
@ -153,95 +154,79 @@ func (u *UsbGadget) GetKeysDownState() KeysDownState {
|
|||
return u.keysDownState
|
||||
}
|
||||
|
||||
func (u *UsbGadget) updateKeyDownState(state KeysDownState) {
|
||||
u.log.Trace().Interface("old", u.keysDownState).Interface("new", state).Msg("acquiring keyboardStateLock for updateKeyDownState")
|
||||
|
||||
// this is intentional to unlock keyboard state lock before onKeysDownChange callback
|
||||
{
|
||||
u.keyboardStateLock.Lock()
|
||||
defer u.keyboardStateLock.Unlock()
|
||||
|
||||
if u.keysDownState.Modifier == state.Modifier &&
|
||||
bytes.Equal(u.keysDownState.Keys, state.Keys) {
|
||||
return // No change in key down state
|
||||
}
|
||||
|
||||
u.log.Trace().Interface("old", u.keysDownState).Interface("new", state).Msg("keysDownState updated")
|
||||
u.keysDownState = state
|
||||
}
|
||||
|
||||
if u.onKeysDownChange != nil {
|
||||
u.log.Trace().Interface("state", state).Msg("calling onKeysDownChange")
|
||||
(*u.onKeysDownChange)(state)
|
||||
u.log.Trace().Interface("state", state).Msg("onKeysDownChange called")
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UsbGadget) SetOnKeysDownChange(f func(state KeysDownState)) {
|
||||
u.onKeysDownChange = &f
|
||||
}
|
||||
|
||||
func (u *UsbGadget) scheduleAutoRelease(key byte) {
|
||||
u.log.Trace().Msg("scheduling autoRelease")
|
||||
u.kbdAutoReleaseLock.Lock()
|
||||
defer unlockWithLog(&u.kbdAutoReleaseLock, u.log, "autoRelease scheduled")
|
||||
|
||||
if u.kbdAutoReleaseTimer != nil {
|
||||
u.kbdAutoReleaseTimer.Stop()
|
||||
if u.kbdAutoReleaseTimers[key] != nil {
|
||||
u.kbdAutoReleaseTimers[key].Stop()
|
||||
}
|
||||
|
||||
u.kbdAutoReleaseTimer = time.AfterFunc(autoReleaseKeyboardInterval, func() {
|
||||
u.performAutoRelease()
|
||||
u.kbdAutoReleaseTimers[key] = time.AfterFunc(autoReleaseKeyboardInterval, func() {
|
||||
u.performAutoRelease(key)
|
||||
})
|
||||
}
|
||||
|
||||
func (u *UsbGadget) cancelAutoRelease() {
|
||||
u.log.Trace().Msg("cancelling autoRelease")
|
||||
func (u *UsbGadget) cancelAutoRelease(key byte) {
|
||||
u.kbdAutoReleaseLock.Lock()
|
||||
defer unlockWithLog(&u.kbdAutoReleaseLock, u.log, "autoRelease cancelled")
|
||||
|
||||
if u.kbdAutoReleaseTimer != nil {
|
||||
u.kbdAutoReleaseTimer.Stop()
|
||||
if timer := u.kbdAutoReleaseTimers[key]; timer != nil {
|
||||
timer.Stop()
|
||||
u.kbdAutoReleaseTimers[key] = nil
|
||||
delete(u.kbdAutoReleaseTimers, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UsbGadget) DelayAutoRelease() {
|
||||
u.log.Trace().Msg("delaying autoRelease")
|
||||
u.kbdAutoReleaseLock.Lock()
|
||||
defer unlockWithLog(&u.kbdAutoReleaseLock, u.log, "autoRelease delayed")
|
||||
|
||||
if u.kbdAutoReleaseTimer == nil {
|
||||
if u.kbdAutoReleaseTimers == nil {
|
||||
return
|
||||
}
|
||||
|
||||
u.kbdAutoReleaseTimer.Reset(autoReleaseKeyboardInterval)
|
||||
|
||||
u.log.Trace().Msg("auto-release timer reset")
|
||||
for _, timer := range u.kbdAutoReleaseTimers {
|
||||
if timer != nil {
|
||||
timer.Reset(autoReleaseKeyboardInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UsbGadget) performAutoRelease() {
|
||||
u.log.Trace().Msg("performing autoRelease")
|
||||
func (u *UsbGadget) performAutoRelease(key byte) {
|
||||
u.kbdAutoReleaseLock.Lock()
|
||||
defer unlockWithLog(&u.kbdAutoReleaseLock, u.log, "autoRelease unlocked")
|
||||
|
||||
key := u.kbdAutoReleaseLastKey
|
||||
|
||||
select {
|
||||
case <-u.keyboardStateCtx.Done():
|
||||
if u.kbdAutoReleaseTimers[key] == nil {
|
||||
u.log.Warn().Uint8("key", key).Msg("autoRelease timer not found")
|
||||
u.kbdAutoReleaseLock.Unlock()
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// we just reset the keyboard state to 0 no matter what
|
||||
u.log.Trace().Uint8("key", key).Msg("auto-releasing keyboard key")
|
||||
_, err := u.keypressReport(key, false, false)
|
||||
if err != nil {
|
||||
u.log.Warn().Uint8("key", key).Msg("failed to auto-release keyboard key")
|
||||
u.kbdAutoReleaseTimers[key].Stop()
|
||||
u.kbdAutoReleaseTimers[key] = nil
|
||||
delete(u.kbdAutoReleaseTimers, key)
|
||||
u.kbdAutoReleaseLock.Unlock()
|
||||
|
||||
// Skip if already released
|
||||
state := u.GetKeysDownState()
|
||||
alreadyReleased := true
|
||||
|
||||
for i := range state.Keys {
|
||||
if state.Keys[i] == key {
|
||||
alreadyReleased = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
u.kbdAutoReleaseTimer = nil
|
||||
if alreadyReleased {
|
||||
return
|
||||
}
|
||||
|
||||
u.log.Trace().Uint8("key", key).Msg("auto release performed")
|
||||
u.keypressReport(key, false)
|
||||
}
|
||||
|
||||
func (u *UsbGadget) listenKeyboardEvents() {
|
||||
|
|
@ -313,7 +298,11 @@ func (u *UsbGadget) OpenKeyboardHidFile() error {
|
|||
return u.openKeyboardHidFile()
|
||||
}
|
||||
|
||||
var keyboardWriteHidFileLock sync.Mutex
|
||||
|
||||
func (u *UsbGadget) keyboardWriteHidFile(modifier byte, keys []byte) error {
|
||||
keyboardWriteHidFileLock.Lock()
|
||||
defer keyboardWriteHidFileLock.Unlock()
|
||||
if err := u.openKeyboardHidFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -337,17 +326,29 @@ func (u *UsbGadget) UpdateKeysDown(modifier byte, keys []byte) KeysDownState {
|
|||
}
|
||||
}
|
||||
|
||||
downState := KeysDownState{
|
||||
state := KeysDownState{
|
||||
Modifier: modifier,
|
||||
Keys: []byte(keys[:]),
|
||||
}
|
||||
u.updateKeyDownState(downState)
|
||||
return downState
|
||||
|
||||
u.keyboardStateLock.Lock()
|
||||
|
||||
if u.keysDownState.Modifier == state.Modifier &&
|
||||
bytes.Equal(u.keysDownState.Keys, state.Keys) {
|
||||
u.keyboardStateLock.Unlock()
|
||||
return state // No change in key down state
|
||||
}
|
||||
|
||||
func (u *UsbGadget) KeyboardReport(modifier byte, keys []byte) (KeysDownState, error) {
|
||||
u.keyboardLock.Lock()
|
||||
defer u.keyboardLock.Unlock()
|
||||
u.keysDownState = state
|
||||
u.keyboardStateLock.Unlock()
|
||||
|
||||
if u.onKeysDownChange != nil {
|
||||
(*u.onKeysDownChange)(state) // this enques to the outgoing hidrpc queue via usb.go → currentSession.enqueueKeysDownState(...)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
func (u *UsbGadget) KeyboardReport(modifier byte, keys []byte) error {
|
||||
defer u.resetUserInputTime()
|
||||
|
||||
if len(keys) > hidKeyBufferSize {
|
||||
|
|
@ -362,7 +363,8 @@ func (u *UsbGadget) KeyboardReport(modifier byte, keys []byte) (KeysDownState, e
|
|||
u.log.Warn().Uint8("modifier", modifier).Uints8("keys", keys).Msg("Could not write keyboard report to hidg0")
|
||||
}
|
||||
|
||||
return u.UpdateKeysDown(modifier, keys), err
|
||||
u.UpdateKeysDown(modifier, keys)
|
||||
return err
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
@ -402,10 +404,10 @@ var KeyCodeToMaskMap = map[byte]byte{
|
|||
RightSuper: ModifierMaskRightSuper,
|
||||
}
|
||||
|
||||
func (u *UsbGadget) keypressReportNonThreadSafe(key byte, press bool, autoRelease bool) (KeysDownState, error) {
|
||||
func (u *UsbGadget) keypressReport(key byte, press bool) (KeysDownState, error) {
|
||||
defer u.resetUserInputTime()
|
||||
|
||||
l := u.log.With().Uint8("key", key).Bool("press", press).Bool("autoRelease", autoRelease).Logger()
|
||||
l := u.log.With().Uint8("key", key).Bool("press", press).Logger()
|
||||
if l.GetLevel() <= zerolog.DebugLevel {
|
||||
requestID := xid.New()
|
||||
l = l.With().Str("requestID", requestID.String()).Logger()
|
||||
|
|
@ -470,64 +472,21 @@ func (u *UsbGadget) keypressReportNonThreadSafe(key byte, press bool, autoReleas
|
|||
}
|
||||
}
|
||||
|
||||
if l.GetLevel() <= zerolog.DebugLevel {
|
||||
l = l.With().Uint8("modifier", modifier).Uints8("keys", keys).Logger()
|
||||
}
|
||||
|
||||
l.Trace().Msg("writing keypress report to hidg0")
|
||||
|
||||
err := u.keyboardWriteHidFile(modifier, keys)
|
||||
if err != nil {
|
||||
l.Warn().Msg("Could not write keypress report to hidg0")
|
||||
}
|
||||
|
||||
l.Trace().Msg("keypress report written to hidg0")
|
||||
|
||||
if press {
|
||||
{
|
||||
l.Trace().Msg("acquiring kbdAutoReleaseLock to update last key")
|
||||
u.kbdAutoReleaseLock.Lock()
|
||||
u.kbdAutoReleaseLastKey = key
|
||||
unlockWithLog(&u.kbdAutoReleaseLock, u.log, "last key updated")
|
||||
}
|
||||
|
||||
if autoRelease {
|
||||
u.scheduleAutoRelease(key)
|
||||
}
|
||||
} else {
|
||||
if autoRelease {
|
||||
u.cancelAutoRelease()
|
||||
}
|
||||
}
|
||||
|
||||
return u.UpdateKeysDown(modifier, keys), err
|
||||
}
|
||||
|
||||
type keypressReportResult struct {
|
||||
KeysDownState KeysDownState
|
||||
Error error
|
||||
func (u *UsbGadget) KeypressReport(key byte, press bool) error {
|
||||
state, err := u.keypressReport(key, press)
|
||||
isRolledOver := state.Keys[0] == hidErrorRollOver
|
||||
|
||||
if isRolledOver {
|
||||
u.cancelAutoRelease(key)
|
||||
} else if press {
|
||||
u.scheduleAutoRelease(key)
|
||||
} else {
|
||||
u.cancelAutoRelease(key)
|
||||
}
|
||||
|
||||
func (u *UsbGadget) keypressReport(key byte, press bool, autoRelease bool) (KeysDownState, error) {
|
||||
u.keyboardLock.Lock()
|
||||
defer u.keyboardLock.Unlock()
|
||||
|
||||
r := make(chan keypressReportResult)
|
||||
go func() {
|
||||
state, err := u.keypressReportNonThreadSafe(key, press, autoRelease)
|
||||
r <- keypressReportResult{KeysDownState: state, Error: err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(1 * time.Second):
|
||||
u.log.Warn().Msg("keypressReport timed out, possibly stuck")
|
||||
return u.keysDownState, fmt.Errorf("keypressReport timed out, possibly stuck")
|
||||
case ret := <-r:
|
||||
u.log.Debug().Msg("keypressReport handled")
|
||||
return ret.KeysDownState, ret.Error
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UsbGadget) KeypressReport(key byte, press bool) (KeysDownState, error) {
|
||||
return u.keypressReport(key, press, true)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,8 +69,7 @@ type UsbGadget struct {
|
|||
keysDownState KeysDownState // keyboard dynamic state (modifier keys and pressed keys)
|
||||
|
||||
kbdAutoReleaseLock sync.Mutex
|
||||
kbdAutoReleaseTimer *time.Timer
|
||||
kbdAutoReleaseLastKey byte
|
||||
kbdAutoReleaseTimers map[byte]*time.Timer
|
||||
|
||||
keyboardStateLock sync.Mutex
|
||||
keyboardStateCtx context.Context
|
||||
|
|
@ -136,6 +135,7 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev
|
|||
keyboardStateCancel: keyboardCancel,
|
||||
keyboardState: 0,
|
||||
keysDownState: KeysDownState{Modifier: 0, Keys: []byte{0, 0, 0, 0, 0, 0}}, // must be initialized to hidKeyBufferSize (6) zero bytes
|
||||
kbdAutoReleaseTimers: make(map[byte]*time.Timer),
|
||||
enabledDevices: *enabledDevices,
|
||||
lastUserInput: time.Now(),
|
||||
log: logger,
|
||||
|
|
@ -163,10 +163,10 @@ func (u *UsbGadget) Close() error {
|
|||
|
||||
// Stop auto-release timer
|
||||
u.kbdAutoReleaseLock.Lock()
|
||||
if u.kbdAutoReleaseTimer != nil {
|
||||
u.kbdAutoReleaseTimer.Stop()
|
||||
u.kbdAutoReleaseTimer = nil
|
||||
for _, timer := range u.kbdAutoReleaseTimers {
|
||||
timer.Stop()
|
||||
}
|
||||
u.kbdAutoReleaseTimers = make(map[byte]*time.Timer)
|
||||
u.kbdAutoReleaseLock.Unlock()
|
||||
|
||||
// Close HID files
|
||||
|
|
|
|||
|
|
@ -1066,9 +1066,7 @@ var rpcHandlers = map[string]RPCHandler{
|
|||
"getNetworkSettings": {Func: rpcGetNetworkSettings},
|
||||
"setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}},
|
||||
"renewDHCPLease": {Func: rpcRenewDHCPLease},
|
||||
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
|
||||
"getKeyboardLedState": {Func: rpcGetKeyboardLedState},
|
||||
"keypressReport": {Func: rpcKeypressReport, Params: []string{"key", "press"}},
|
||||
"getKeyDownState": {Func: rpcGetKeysDownState},
|
||||
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
|
||||
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
|
||||
|
|
|
|||
5
mdns.go
5
mdns.go
|
|
@ -13,10 +13,7 @@ func initMdns() error {
|
|||
networkState.GetHostname(),
|
||||
networkState.GetFQDN(),
|
||||
},
|
||||
ListenOptions: &mdns.MDNSListenOptions{
|
||||
IPv4: true,
|
||||
IPv6: true,
|
||||
},
|
||||
ListenOptions: config.NetworkConfig.GetMDNSMode(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export default function Ipv6NetworkCard({
|
|||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-2">
|
||||
{networkState?.dhcp_lease?.ip && (
|
||||
{networkState?.ipv6_link_local && (
|
||||
<div className="flex flex-col justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Link-local
|
||||
|
|
|
|||
|
|
@ -237,6 +237,7 @@ export default function WebRTCVideo() {
|
|||
const keyDownHandler = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
if (e.repeat) return;
|
||||
const code = getAdjustedKeyCode(e);
|
||||
const hidKey = keys[code];
|
||||
|
||||
|
|
|
|||
|
|
@ -36,11 +36,16 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
|||
}, [rpcHidChannel, rpcHidProtocolVersion]);
|
||||
|
||||
const rpcHidUnreliableReady = useMemo(() => {
|
||||
return rpcHidUnreliableChannel?.readyState === "open" && rpcHidProtocolVersion !== null;
|
||||
return (
|
||||
rpcHidUnreliableChannel?.readyState === "open" && rpcHidProtocolVersion !== null
|
||||
);
|
||||
}, [rpcHidUnreliableChannel, rpcHidProtocolVersion]);
|
||||
|
||||
const rpcHidUnreliableNonOrderedReady = useMemo(() => {
|
||||
return rpcHidUnreliableNonOrderedChannel?.readyState === "open" && rpcHidProtocolVersion !== null;
|
||||
return (
|
||||
rpcHidUnreliableNonOrderedChannel?.readyState === "open" &&
|
||||
rpcHidProtocolVersion !== null
|
||||
);
|
||||
}, [rpcHidUnreliableNonOrderedChannel, rpcHidProtocolVersion]);
|
||||
|
||||
const rpcHidStatus = useMemo(() => {
|
||||
|
|
@ -50,7 +55,15 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
|||
return `ready (v${rpcHidProtocolVersion}${rpcHidUnreliableReady ? "+u" : ""})`;
|
||||
}, [rpcHidChannel, rpcHidUnreliableReady, rpcHidProtocolVersion]);
|
||||
|
||||
const sendMessage = useCallback((message: RpcMessage, { ignoreHandshakeState, useUnreliableChannel, requireOrdered = true }: sendMessageParams = {}) => {
|
||||
const sendMessage = useCallback(
|
||||
(
|
||||
message: RpcMessage,
|
||||
{
|
||||
ignoreHandshakeState,
|
||||
useUnreliableChannel,
|
||||
requireOrdered = true,
|
||||
}: sendMessageParams = {},
|
||||
) => {
|
||||
if (rpcHidChannel?.readyState !== "open") return;
|
||||
if (!rpcHidReady && !ignoreHandshakeState) return;
|
||||
|
||||
|
|
@ -72,19 +85,22 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
|||
}
|
||||
|
||||
rpcHidChannel?.send(data as unknown as ArrayBuffer);
|
||||
}, [
|
||||
},
|
||||
[
|
||||
rpcHidChannel,
|
||||
rpcHidUnreliableChannel,
|
||||
rpcHidUnreliableNonOrderedChannel,
|
||||
rpcHidReady,
|
||||
rpcHidUnreliableReady,
|
||||
rpcHidUnreliableNonOrderedReady,
|
||||
]);
|
||||
],
|
||||
);
|
||||
|
||||
const reportKeyboardEvent = useCallback(
|
||||
(keys: number[], modifier: number) => {
|
||||
sendMessage(new KeyboardReportMessage(keys, modifier));
|
||||
}, [sendMessage],
|
||||
},
|
||||
[sendMessage],
|
||||
);
|
||||
|
||||
const reportKeypressEvent = useCallback(
|
||||
|
|
@ -96,7 +112,9 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
|||
|
||||
const reportAbsMouseEvent = useCallback(
|
||||
(x: number, y: number, buttons: number) => {
|
||||
sendMessage(new PointerReportMessage(x, y, buttons), { useUnreliableChannel: true });
|
||||
sendMessage(new PointerReportMessage(x, y, buttons), {
|
||||
useUnreliableChannel: true,
|
||||
});
|
||||
},
|
||||
[sendMessage],
|
||||
);
|
||||
|
|
@ -109,7 +127,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
|||
);
|
||||
|
||||
const reportKeypressKeepAlive = useCallback(() => {
|
||||
sendMessage(KEEPALIVE_MESSAGE, { useUnreliableChannel: true, requireOrdered: false });
|
||||
sendMessage(KEEPALIVE_MESSAGE);
|
||||
}, [sendMessage]);
|
||||
|
||||
const sendHandshake = useCallback(() => {
|
||||
|
|
@ -119,7 +137,8 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
|||
sendMessage(new HandshakeMessage(HID_RPC_VERSION), { ignoreHandshakeState: true });
|
||||
}, [rpcHidChannel, rpcHidProtocolVersion, sendMessage]);
|
||||
|
||||
const handleHandshake = useCallback((message: HandshakeMessage) => {
|
||||
const handleHandshake = useCallback(
|
||||
(message: HandshakeMessage) => {
|
||||
if (!message.version) {
|
||||
console.error("Received handshake message without version", message);
|
||||
return;
|
||||
|
|
@ -134,7 +153,9 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
|||
}
|
||||
|
||||
setRpcHidProtocolVersion(message.version);
|
||||
}, [setRpcHidProtocolVersion]);
|
||||
},
|
||||
[setRpcHidProtocolVersion],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rpcHidChannel) return;
|
||||
|
|
@ -168,12 +189,12 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
|||
};
|
||||
|
||||
const openHandler = () => {
|
||||
console.warn("HID RPC channel opened");
|
||||
console.info("HID RPC channel opened");
|
||||
sendHandshake();
|
||||
};
|
||||
|
||||
const closeHandler = () => {
|
||||
console.warn("HID RPC channel closed");
|
||||
console.info("HID RPC channel closed");
|
||||
setRpcHidProtocolVersion(null);
|
||||
};
|
||||
|
||||
|
|
@ -186,15 +207,13 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
|||
rpcHidChannel.removeEventListener("close", closeHandler);
|
||||
rpcHidChannel.removeEventListener("open", openHandler);
|
||||
};
|
||||
},
|
||||
[
|
||||
}, [
|
||||
rpcHidChannel,
|
||||
onHidRpcMessage,
|
||||
setRpcHidProtocolVersion,
|
||||
sendHandshake,
|
||||
handleHandshake,
|
||||
],
|
||||
);
|
||||
]);
|
||||
|
||||
return {
|
||||
reportKeyboardEvent,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import { useCallback, useRef } from "react";
|
||||
|
||||
import { hidErrorRollOver, hidKeyBufferSize, KeysDownState, useHidStore, useRTCStore } from "@/hooks/stores";
|
||||
import {
|
||||
hidErrorRollOver,
|
||||
hidKeyBufferSize,
|
||||
KeysDownState,
|
||||
useHidStore,
|
||||
useRTCStore,
|
||||
} from "@/hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { useHidRpc } from "@/hooks/useHidRpc";
|
||||
import { KeyboardLedStateMessage, KeysDownStateMessage } from "@/hooks/hidRpc";
|
||||
|
|
@ -31,7 +37,7 @@ export default function useKeyboard() {
|
|||
reportKeypressEvent: sendKeypressEventHidRpc,
|
||||
reportKeypressKeepAlive: sendKeypressKeepAliveHidRpc,
|
||||
rpcHidReady,
|
||||
} = useHidRpc((message) => {
|
||||
} = useHidRpc(message => {
|
||||
switch (message.constructor) {
|
||||
case KeysDownStateMessage:
|
||||
setKeysDownState((message as KeysDownStateMessage).keysDownState);
|
||||
|
|
@ -52,7 +58,9 @@ export default function useKeyboard() {
|
|||
async (state: KeysDownState) => {
|
||||
if (rpcDataChannel?.readyState !== "open" && !rpcHidReady) return;
|
||||
|
||||
console.debug(`Send keyboardReport keys: ${state.keys}, modifier: ${state.modifier}`);
|
||||
console.debug(
|
||||
`Send keyboardReport keys: ${state.keys}, modifier: ${state.modifier}`,
|
||||
);
|
||||
|
||||
if (rpcHidReady) {
|
||||
console.debug("Sending keyboard report via HidRPC");
|
||||
|
|
@ -60,20 +68,18 @@ export default function useKeyboard() {
|
|||
return;
|
||||
}
|
||||
|
||||
send("keyboardReport", { keys: state.keys, modifier: state.modifier }, (resp: JsonRpcResponse) => {
|
||||
send(
|
||||
"keyboardReport",
|
||||
{ keys: state.keys, modifier: state.modifier },
|
||||
(resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
console.error(`Failed to send keyboard report ${state}`, resp.error);
|
||||
}
|
||||
});
|
||||
},
|
||||
[
|
||||
rpcDataChannel?.readyState,
|
||||
rpcHidReady,
|
||||
send,
|
||||
sendKeyboardEventHidRpc,
|
||||
],
|
||||
);
|
||||
|
||||
},
|
||||
[rpcDataChannel?.readyState, rpcHidReady, send, sendKeyboardEventHidRpc],
|
||||
);
|
||||
|
||||
// executeMacro is used to execute a macro consisting of multiple steps.
|
||||
// Each step can have multiple keys, multiple modifiers and a delay.
|
||||
|
|
@ -81,10 +87,14 @@ export default function useKeyboard() {
|
|||
// After the delay, the keys and modifiers are released and the next step is executed.
|
||||
// If a step has no keys or modifiers, it is treated as a delay-only step.
|
||||
// A small pause is added between steps to ensure that the device can process the events.
|
||||
const executeMacro = async (steps: { keys: string[] | null; modifiers: string[] | null; delay: number }[]) => {
|
||||
const executeMacro = async (
|
||||
steps: { keys: string[] | null; modifiers: string[] | null; delay: number }[],
|
||||
) => {
|
||||
for (const [index, step] of steps.entries()) {
|
||||
const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean);
|
||||
const modifierMask: number = (step.modifiers || []).map(mod => modifiers[mod]).reduce((acc, val) => acc + val, 0);
|
||||
const modifierMask: number = (step.modifiers || [])
|
||||
.map(mod => modifiers[mod])
|
||||
.reduce((acc, val) => acc + val, 0);
|
||||
|
||||
// If the step has keys and/or modifiers, press them and hold for the delay
|
||||
if (keyValues.length > 0 || modifierMask > 0) {
|
||||
|
|
@ -119,6 +129,8 @@ export default function useKeyboard() {
|
|||
clearInterval(keepAliveTimerRef.current);
|
||||
}
|
||||
|
||||
sendKeypressKeepAliveHidRpc();
|
||||
|
||||
// Create new interval timer
|
||||
keepAliveTimerRef.current = setInterval(() => {
|
||||
sendKeypressKeepAliveHidRpc();
|
||||
|
|
@ -174,7 +186,11 @@ export default function useKeyboard() {
|
|||
sendKeypress(key, press);
|
||||
} else {
|
||||
// if the keyPress api is not available, we need to handle the key locally
|
||||
const downState = simulateDeviceSideKeyHandlingForLegacyDevices(keysDownState, key, press);
|
||||
const downState = simulateDeviceSideKeyHandlingForLegacyDevices(
|
||||
keysDownState,
|
||||
key,
|
||||
press,
|
||||
);
|
||||
sendKeyboardEvent(downState); // then we send the full state
|
||||
|
||||
// if we just sent ErrorRollOver, reset to empty state
|
||||
|
|
@ -194,7 +210,11 @@ export default function useKeyboard() {
|
|||
);
|
||||
|
||||
// IMPORTANT: See the keyPressReportApiAvailable comment above for the reason this exists
|
||||
function simulateDeviceSideKeyHandlingForLegacyDevices(state: KeysDownState, key: number, press: boolean): KeysDownState {
|
||||
function simulateDeviceSideKeyHandlingForLegacyDevices(
|
||||
state: KeysDownState,
|
||||
key: number,
|
||||
press: boolean,
|
||||
): KeysDownState {
|
||||
// IMPORTANT: This code parallels the logic in the kernel's hid-gadget driver
|
||||
// for handling key presses and releases. It ensures that the USB gadget
|
||||
// behaves similarly to a real USB HID keyboard. This logic is paralleled
|
||||
|
|
@ -223,7 +243,7 @@ export default function useKeyboard() {
|
|||
// and if we find a zero byte, we can place the key there (if press is true)
|
||||
if (keys[i] === key || keys[i] === 0) {
|
||||
if (press) {
|
||||
keys[i] = key // overwrites the zero byte or the same key if already pressed
|
||||
keys[i] = key; // overwrites the zero byte or the same key if already pressed
|
||||
} else {
|
||||
// we are releasing the key, remove it from the buffer
|
||||
if (keys[i] !== 0) {
|
||||
|
|
@ -239,13 +259,15 @@ export default function useKeyboard() {
|
|||
// If we reach here it means we didn't find an empty slot or the key in the buffer
|
||||
if (overrun) {
|
||||
if (press) {
|
||||
console.warn(`keyboard buffer overflow current keys ${keys}, key: ${key} not added`);
|
||||
console.warn(
|
||||
`keyboard buffer overflow current keys ${keys}, key: ${key} not added`,
|
||||
);
|
||||
// Fill all key slots with ErrorRollOver (0x01) to indicate overflow
|
||||
keys.length = hidKeyBufferSize;
|
||||
keys.fill(hidErrorRollOver);
|
||||
} else {
|
||||
// If we are releasing a key, and we didn't find it in a slot, who cares?
|
||||
console.debug(`key ${key} not found in buffer, nothing to release`)
|
||||
console.debug(`key ${key} not found in buffer, nothing to release`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,11 +166,11 @@ export default function SettingsNetworkRoute() {
|
|||
}, [getNetworkState, getNetworkSettings]);
|
||||
|
||||
const handleIpv4ModeChange = (value: IPv4Mode | string) => {
|
||||
setNetworkSettings({ ...networkSettings, ipv4_mode: value as IPv4Mode });
|
||||
setNetworkSettingsRemote({ ...networkSettings, ipv4_mode: value as IPv4Mode });
|
||||
};
|
||||
|
||||
const handleIpv6ModeChange = (value: IPv6Mode | string) => {
|
||||
setNetworkSettings({ ...networkSettings, ipv6_mode: value as IPv6Mode });
|
||||
setNetworkSettingsRemote({ ...networkSettings, ipv6_mode: value as IPv6Mode });
|
||||
};
|
||||
|
||||
const handleLldpModeChange = (value: LLDPMode | string) => {
|
||||
|
|
@ -419,7 +419,7 @@ export default function SettingsNetworkRoute() {
|
|||
value={networkSettings.ipv6_mode}
|
||||
onChange={e => handleIpv6ModeChange(e.target.value)}
|
||||
options={filterUnknown([
|
||||
// { value: "disabled", label: "Disabled" },
|
||||
{ value: "disabled", label: "Disabled" },
|
||||
{ value: "slaac", label: "SLAAC" },
|
||||
// { value: "dhcpv6", label: "DHCPv6" },
|
||||
// { value: "slaac_and_dhcpv6", label: "SLAAC and DHCPv6" },
|
||||
|
|
|
|||
6
usb.go
6
usb.go
|
|
@ -33,7 +33,7 @@ func initUsbGadget() {
|
|||
|
||||
gadget.SetOnKeysDownChange(func(state usbgadget.KeysDownState) {
|
||||
if currentSession != nil {
|
||||
currentSession.reportHidRPCKeysDownState(state)
|
||||
currentSession.enqueueKeysDownState(state)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -43,11 +43,11 @@ func initUsbGadget() {
|
|||
}
|
||||
}
|
||||
|
||||
func rpcKeyboardReport(modifier byte, keys []byte) (usbgadget.KeysDownState, error) {
|
||||
func rpcKeyboardReport(modifier byte, keys []byte) error {
|
||||
return gadget.KeyboardReport(modifier, keys)
|
||||
}
|
||||
|
||||
func rpcKeypressReport(key byte, press bool) (usbgadget.KeysDownState, error) {
|
||||
func rpcKeypressReport(key byte, press bool) error {
|
||||
return gadget.KeypressReport(key, press)
|
||||
}
|
||||
|
||||
|
|
|
|||
25
web.go
25
web.go
|
|
@ -562,14 +562,31 @@ func RunWebServer() {
|
|||
r := setupRouter()
|
||||
|
||||
// Determine the binding address based on the config
|
||||
bindAddress := ":80" // Default to all interfaces
|
||||
var bindAddress string
|
||||
listenPort := 80 // default port
|
||||
useIPv4 := config.NetworkConfig.IPv4Mode.String != "disabled"
|
||||
useIPv6 := config.NetworkConfig.IPv6Mode.String != "disabled"
|
||||
|
||||
if config.LocalLoopbackOnly {
|
||||
bindAddress = "localhost:80" // Loopback only (both IPv4 and IPv6)
|
||||
if useIPv4 && useIPv6 {
|
||||
bindAddress = fmt.Sprintf("localhost:%d", listenPort)
|
||||
} else if useIPv4 {
|
||||
bindAddress = fmt.Sprintf("127.0.0.1:%d", listenPort)
|
||||
} else if useIPv6 {
|
||||
bindAddress = fmt.Sprintf("[::1]:%d", listenPort)
|
||||
}
|
||||
} else {
|
||||
if useIPv4 && useIPv6 {
|
||||
bindAddress = fmt.Sprintf(":%d", listenPort)
|
||||
} else if useIPv4 {
|
||||
bindAddress = fmt.Sprintf("0.0.0.0:%d", listenPort)
|
||||
} else if useIPv6 {
|
||||
bindAddress = fmt.Sprintf("[::]:%d", listenPort)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info().Str("bindAddress", bindAddress).Bool("loopbackOnly", config.LocalLoopbackOnly).Msg("Starting web server")
|
||||
err := r.Run(bindAddress)
|
||||
if err != nil {
|
||||
if err := r.Run(bindAddress); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
webrtc.go
30
webrtc.go
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
"github.com/jetkvm/kvm/internal/hidrpc"
|
||||
"github.com/jetkvm/kvm/internal/logging"
|
||||
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
|
@ -30,6 +31,8 @@ type Session struct {
|
|||
hidRPCAvailable bool
|
||||
hidQueueLock sync.Mutex
|
||||
hidQueue []chan hidQueueMessage
|
||||
|
||||
keysDownStateQueue chan usbgadget.KeysDownState
|
||||
}
|
||||
|
||||
type hidQueueMessage struct {
|
||||
|
|
@ -96,6 +99,32 @@ func (s *Session) handleQueues(index int) {
|
|||
}
|
||||
}
|
||||
|
||||
const keysDownStateQueueSize = 256
|
||||
|
||||
func (s *Session) initKeysDownStateQueue() {
|
||||
// serialise outbound key state reports so unreliable links can't stall input handling
|
||||
s.keysDownStateQueue = make(chan usbgadget.KeysDownState, keysDownStateQueueSize)
|
||||
go s.handleKeysDownStateQueue()
|
||||
}
|
||||
|
||||
func (s *Session) handleKeysDownStateQueue() {
|
||||
for state := range s.keysDownStateQueue {
|
||||
s.reportHidRPCKeysDownState(state)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) enqueueKeysDownState(state usbgadget.KeysDownState) {
|
||||
if s == nil || s.keysDownStateQueue == nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case s.keysDownStateQueue <- state:
|
||||
default:
|
||||
hidRPCLogger.Warn().Msg("dropping keys down state update; queue full")
|
||||
}
|
||||
}
|
||||
|
||||
func getOnHidMessageHandler(session *Session, scopedLogger *zerolog.Logger, channel string) func(msg webrtc.DataChannelMessage) {
|
||||
return func(msg webrtc.DataChannelMessage) {
|
||||
l := scopedLogger.With().
|
||||
|
|
@ -181,6 +210,7 @@ func newSession(config SessionConfig) (*Session, error) {
|
|||
session := &Session{peerConnection: peerConnection}
|
||||
session.rpcQueue = make(chan webrtc.DataChannelMessage, 256)
|
||||
session.initQueues()
|
||||
session.initKeysDownStateQueue()
|
||||
|
||||
go func() {
|
||||
for msg := range session.rpcQueue {
|
||||
|
|
|
|||
Loading…
Reference in New Issue