Compare commits

..

1 Commits

Author SHA1 Message Date
Aveline 761689f216
Merge 12210fac96 into 1717549578 2025-09-12 12:20:08 -05:00
17 changed files with 293 additions and 327 deletions

View File

@ -25,7 +25,11 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
} }
session.hidRPCAvailable = true session.hidRPCAvailable = true
case hidrpc.TypeKeypressReport, hidrpc.TypeKeyboardReport: case hidrpc.TypeKeypressReport, hidrpc.TypeKeyboardReport:
rpcErr = handleHidRPCKeyboardInput(message) keysDownState, err := handleHidRPCKeyboardInput(message)
if keysDownState != nil {
session.reportHidRPCKeysDownState(*keysDownState)
}
rpcErr = err
case hidrpc.TypeKeypressKeepAliveReport: case hidrpc.TypeKeypressKeepAliveReport:
gadget.DelayAutoRelease() gadget.DelayAutoRelease()
case hidrpc.TypePointerReport: case hidrpc.TypePointerReport:
@ -91,25 +95,27 @@ func onHidMessage(msg hidQueueMessage, session *Session) {
} }
} }
func handleHidRPCKeyboardInput(message hidrpc.Message) error { func handleHidRPCKeyboardInput(message hidrpc.Message) (*usbgadget.KeysDownState, error) {
switch message.Type() { switch message.Type() {
case hidrpc.TypeKeypressReport: case hidrpc.TypeKeypressReport:
keypressReport, err := message.KeypressReport() keypressReport, err := message.KeypressReport()
if err != nil { if err != nil {
logger.Warn().Err(err).Msg("failed to get keypress report") logger.Warn().Err(err).Msg("failed to get keypress report")
return err return nil, err
} }
return rpcKeypressReport(keypressReport.Key, keypressReport.Press) keysDownState, rpcError := rpcKeypressReport(keypressReport.Key, keypressReport.Press)
return &keysDownState, rpcError
case hidrpc.TypeKeyboardReport: case hidrpc.TypeKeyboardReport:
keyboardReport, err := message.KeyboardReport() keyboardReport, err := message.KeyboardReport()
if err != nil { if err != nil {
logger.Warn().Err(err).Msg("failed to get keyboard report") logger.Warn().Err(err).Msg("failed to get keyboard report")
return err return nil, err
} }
return rpcKeyboardReport(keyboardReport.Modifier, keyboardReport.Keys) keysDownState, rpcError := rpcKeyboardReport(keyboardReport.Modifier, keyboardReport.Keys)
return &keysDownState, rpcError
} }
return fmt.Errorf("unknown HID RPC message type: %d", message.Type()) return nil, fmt.Errorf("unknown HID RPC message type: %d", message.Type())
} }
func reportHidRPC(params any, session *Session) { func reportHidRPC(params any, session *Session) {

View File

@ -99,3 +99,11 @@ func NewKeydownStateMessage(state usbgadget.KeysDownState) *Message {
d: data, d: data,
} }
} }
// NewKeypressKeepAliveMessage creates a new keypress keep alive message.
func NewKeypressKeepAliveMessage() *Message {
return &Message{
t: TypeKeypressKeepAliveReport,
d: []byte{},
}
}

View File

@ -56,12 +56,13 @@ type NetworkConfig struct {
} }
func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions { func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {
mode := c.MDNSMode.String
listenOptions := &mdns.MDNSListenOptions{ listenOptions := &mdns.MDNSListenOptions{
IPv4: c.IPv4Mode.String != "disabled", IPv4: true,
IPv6: c.IPv6Mode.String != "disabled", IPv6: true,
} }
switch c.MDNSMode.String { switch mode {
case "ipv4_only": case "ipv4_only":
listenOptions.IPv6 = false listenOptions.IPv6 = false
case "ipv6_only": case "ipv6_only":

View File

@ -239,10 +239,6 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
ipv4Addresses = append(ipv4Addresses, addr.IP) ipv4Addresses = append(ipv4Addresses, addr.IP)
ipv4AddressesString = append(ipv4AddressesString, addr.IPNet.String()) ipv4AddressesString = append(ipv4AddressesString, addr.IPNet.String())
} else if addr.IP.To16() != nil { } else if addr.IP.To16() != nil {
if s.config.IPv6Mode.String == "disabled" {
continue
}
scopedLogger := s.l.With().Str("ipv6", addr.IP.String()).Logger() scopedLogger := s.l.With().Str("ipv6", addr.IP.String()).Logger()
// check if it's a link local address // check if it's a link local address
if addr.IP.IsLinkLocalUnicast() { if addr.IP.IsLinkLocalUnicast() {
@ -291,7 +287,6 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
} }
s.ipv4Addresses = ipv4AddressesString s.ipv4Addresses = ipv4AddressesString
if s.config.IPv6Mode.String != "disabled" {
if ipv6LinkLocal != nil { if ipv6LinkLocal != nil {
if s.ipv6LinkLocal == nil || s.ipv6LinkLocal.String() != ipv6LinkLocal.String() { if s.ipv6LinkLocal == nil || s.ipv6LinkLocal.String() != ipv6LinkLocal.String() {
scopedLogger := s.l.With().Str("ipv6", ipv6LinkLocal.String()).Logger() scopedLogger := s.l.With().Str("ipv6", ipv6LinkLocal.String()).Logger()
@ -323,7 +318,6 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
changed = true changed = true
} }
} }
}
// if it's the initial check, we'll set changed to false // if it's the initial check, we'll set changed to false
initialCheck := !s.checked initialCheck := !s.checked

View File

@ -65,7 +65,7 @@ func (s *NetworkInterfaceState) IPv6LinkLocalAddress() string {
func (s *NetworkInterfaceState) RpcGetNetworkState() RpcNetworkState { func (s *NetworkInterfaceState) RpcGetNetworkState() RpcNetworkState {
ipv6Addresses := make([]RpcIPv6Address, 0) ipv6Addresses := make([]RpcIPv6Address, 0)
if s.ipv6Addresses != nil && s.config.IPv6Mode.String != "disabled" { if s.ipv6Addresses != nil {
for _, addr := range s.ipv6Addresses { for _, addr := range s.ipv6Addresses {
ipv6Addresses = append(ipv6Addresses, RpcIPv6Address{ ipv6Addresses = append(ipv6Addresses, RpcIPv6Address{
Address: addr.Prefix.String(), Address: addr.Prefix.String(),

View File

@ -5,7 +5,6 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"sync"
"time" "time"
"github.com/rs/xid" "github.com/rs/xid"
@ -28,7 +27,7 @@ var keyboardConfig = gadgetConfigItem{
// macOS default: 15 * 15 = 225ms https://discussions.apple.com/thread/1316947?sortBy=rank // macOS default: 15 * 15 = 225ms https://discussions.apple.com/thread/1316947?sortBy=rank
// Linux default: 250ms https://man.archlinux.org/man/kbdrate.8.en // Linux default: 250ms https://man.archlinux.org/man/kbdrate.8.en
// Windows default: 1s `HKEY_CURRENT_USER\Control Panel\Accessibility\Keyboard Response\AutoRepeatDelay` // Windows default: 1ms `HKEY_CURRENT_USER\Control Panel\Accessibility\Keyboard Response\AutoRepeatDelay`
const autoReleaseKeyboardInterval = time.Millisecond * 100 const autoReleaseKeyboardInterval = time.Millisecond * 100
// Source: https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt // Source: https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt
@ -154,79 +153,95 @@ func (u *UsbGadget) GetKeysDownState() KeysDownState {
return u.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)) { func (u *UsbGadget) SetOnKeysDownChange(f func(state KeysDownState)) {
u.onKeysDownChange = &f u.onKeysDownChange = &f
} }
func (u *UsbGadget) scheduleAutoRelease(key byte) { func (u *UsbGadget) scheduleAutoRelease(key byte) {
u.log.Trace().Msg("scheduling autoRelease")
u.kbdAutoReleaseLock.Lock() u.kbdAutoReleaseLock.Lock()
defer unlockWithLog(&u.kbdAutoReleaseLock, u.log, "autoRelease scheduled") defer unlockWithLog(&u.kbdAutoReleaseLock, u.log, "autoRelease scheduled")
if u.kbdAutoReleaseTimers[key] != nil { if u.kbdAutoReleaseTimer != nil {
u.kbdAutoReleaseTimers[key].Stop() u.kbdAutoReleaseTimer.Stop()
} }
u.kbdAutoReleaseTimers[key] = time.AfterFunc(autoReleaseKeyboardInterval, func() { u.kbdAutoReleaseTimer = time.AfterFunc(autoReleaseKeyboardInterval, func() {
u.performAutoRelease(key) u.performAutoRelease()
}) })
} }
func (u *UsbGadget) cancelAutoRelease(key byte) { func (u *UsbGadget) cancelAutoRelease() {
u.log.Trace().Msg("cancelling autoRelease")
u.kbdAutoReleaseLock.Lock() u.kbdAutoReleaseLock.Lock()
defer unlockWithLog(&u.kbdAutoReleaseLock, u.log, "autoRelease cancelled") defer unlockWithLog(&u.kbdAutoReleaseLock, u.log, "autoRelease cancelled")
if timer := u.kbdAutoReleaseTimers[key]; timer != nil { if u.kbdAutoReleaseTimer != nil {
timer.Stop() u.kbdAutoReleaseTimer.Stop()
u.kbdAutoReleaseTimers[key] = nil
delete(u.kbdAutoReleaseTimers, key)
} }
} }
func (u *UsbGadget) DelayAutoRelease() { func (u *UsbGadget) DelayAutoRelease() {
u.log.Trace().Msg("delaying autoRelease")
u.kbdAutoReleaseLock.Lock() u.kbdAutoReleaseLock.Lock()
defer unlockWithLog(&u.kbdAutoReleaseLock, u.log, "autoRelease delayed") defer unlockWithLog(&u.kbdAutoReleaseLock, u.log, "autoRelease delayed")
if u.kbdAutoReleaseTimers == nil { if u.kbdAutoReleaseTimer == nil {
return return
} }
for _, timer := range u.kbdAutoReleaseTimers { u.kbdAutoReleaseTimer.Reset(autoReleaseKeyboardInterval)
if timer != nil {
timer.Reset(autoReleaseKeyboardInterval) u.log.Trace().Msg("auto-release timer reset")
}
}
} }
func (u *UsbGadget) performAutoRelease(key byte) { func (u *UsbGadget) performAutoRelease() {
u.log.Trace().Msg("performing autoRelease")
u.kbdAutoReleaseLock.Lock() u.kbdAutoReleaseLock.Lock()
defer unlockWithLog(&u.kbdAutoReleaseLock, u.log, "autoRelease unlocked")
if u.kbdAutoReleaseTimers[key] == nil { key := u.kbdAutoReleaseLastKey
u.log.Warn().Uint8("key", key).Msg("autoRelease timer not found")
u.kbdAutoReleaseLock.Unlock() select {
case <-u.keyboardStateCtx.Done():
return return
default:
} }
u.kbdAutoReleaseTimers[key].Stop() // we just reset the keyboard state to 0 no matter what
u.kbdAutoReleaseTimers[key] = nil u.log.Trace().Uint8("key", key).Msg("auto-releasing keyboard key")
delete(u.kbdAutoReleaseTimers, key) _, err := u.keypressReport(key, false, false)
u.kbdAutoReleaseLock.Unlock() if err != nil {
u.log.Warn().Uint8("key", key).Msg("failed to auto-release keyboard key")
// Skip if already released
state := u.GetKeysDownState()
alreadyReleased := true
for i := range state.Keys {
if state.Keys[i] == key {
alreadyReleased = false
break
}
} }
if alreadyReleased { u.kbdAutoReleaseTimer = nil
return
}
u.keypressReport(key, false) u.log.Trace().Uint8("key", key).Msg("auto release performed")
} }
func (u *UsbGadget) listenKeyboardEvents() { func (u *UsbGadget) listenKeyboardEvents() {
@ -298,11 +313,7 @@ func (u *UsbGadget) OpenKeyboardHidFile() error {
return u.openKeyboardHidFile() return u.openKeyboardHidFile()
} }
var keyboardWriteHidFileLock sync.Mutex
func (u *UsbGadget) keyboardWriteHidFile(modifier byte, keys []byte) error { func (u *UsbGadget) keyboardWriteHidFile(modifier byte, keys []byte) error {
keyboardWriteHidFileLock.Lock()
defer keyboardWriteHidFileLock.Unlock()
if err := u.openKeyboardHidFile(); err != nil { if err := u.openKeyboardHidFile(); err != nil {
return err return err
} }
@ -326,29 +337,17 @@ func (u *UsbGadget) UpdateKeysDown(modifier byte, keys []byte) KeysDownState {
} }
} }
state := KeysDownState{ downState := KeysDownState{
Modifier: modifier, Modifier: modifier,
Keys: []byte(keys[:]), Keys: []byte(keys[:]),
} }
u.updateKeyDownState(downState)
u.keyboardStateLock.Lock() return downState
if u.keysDownState.Modifier == state.Modifier &&
bytes.Equal(u.keysDownState.Keys, state.Keys) {
u.keyboardStateLock.Unlock()
return state // No change in key down state
}
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 { func (u *UsbGadget) KeyboardReport(modifier byte, keys []byte) (KeysDownState, error) {
u.keyboardLock.Lock()
defer u.keyboardLock.Unlock()
defer u.resetUserInputTime() defer u.resetUserInputTime()
if len(keys) > hidKeyBufferSize { if len(keys) > hidKeyBufferSize {
@ -363,8 +362,7 @@ func (u *UsbGadget) KeyboardReport(modifier byte, keys []byte) error {
u.log.Warn().Uint8("modifier", modifier).Uints8("keys", keys).Msg("Could not write keyboard report to hidg0") u.log.Warn().Uint8("modifier", modifier).Uints8("keys", keys).Msg("Could not write keyboard report to hidg0")
} }
u.UpdateKeysDown(modifier, keys) return u.UpdateKeysDown(modifier, keys), err
return err
} }
const ( const (
@ -404,10 +402,10 @@ var KeyCodeToMaskMap = map[byte]byte{
RightSuper: ModifierMaskRightSuper, RightSuper: ModifierMaskRightSuper,
} }
func (u *UsbGadget) keypressReport(key byte, press bool) (KeysDownState, error) { func (u *UsbGadget) keypressReportNonThreadSafe(key byte, press bool, autoRelease bool) (KeysDownState, error) {
defer u.resetUserInputTime() defer u.resetUserInputTime()
l := u.log.With().Uint8("key", key).Bool("press", press).Logger() l := u.log.With().Uint8("key", key).Bool("press", press).Bool("autoRelease", autoRelease).Logger()
if l.GetLevel() <= zerolog.DebugLevel { if l.GetLevel() <= zerolog.DebugLevel {
requestID := xid.New() requestID := xid.New()
l = l.With().Str("requestID", requestID.String()).Logger() l = l.With().Str("requestID", requestID.String()).Logger()
@ -472,21 +470,64 @@ func (u *UsbGadget) keypressReport(key byte, press bool) (KeysDownState, error)
} }
} }
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) 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 return u.UpdateKeysDown(modifier, keys), err
} }
func (u *UsbGadget) KeypressReport(key byte, press bool) error { type keypressReportResult struct {
state, err := u.keypressReport(key, press) KeysDownState KeysDownState
isRolledOver := state.Keys[0] == hidErrorRollOver Error error
}
if isRolledOver {
u.cancelAutoRelease(key) func (u *UsbGadget) keypressReport(key byte, press bool, autoRelease bool) (KeysDownState, error) {
} else if press { u.keyboardLock.Lock()
u.scheduleAutoRelease(key) defer u.keyboardLock.Unlock()
} else {
u.cancelAutoRelease(key) r := make(chan keypressReportResult)
} go func() {
state, err := u.keypressReportNonThreadSafe(key, press, autoRelease)
return err 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)
} }

View File

@ -69,7 +69,8 @@ type UsbGadget struct {
keysDownState KeysDownState // keyboard dynamic state (modifier keys and pressed keys) keysDownState KeysDownState // keyboard dynamic state (modifier keys and pressed keys)
kbdAutoReleaseLock sync.Mutex kbdAutoReleaseLock sync.Mutex
kbdAutoReleaseTimers map[byte]*time.Timer kbdAutoReleaseTimer *time.Timer
kbdAutoReleaseLastKey byte
keyboardStateLock sync.Mutex keyboardStateLock sync.Mutex
keyboardStateCtx context.Context keyboardStateCtx context.Context
@ -135,7 +136,6 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev
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 hidKeyBufferSize (6) zero bytes
kbdAutoReleaseTimers: make(map[byte]*time.Timer),
enabledDevices: *enabledDevices, enabledDevices: *enabledDevices,
lastUserInput: time.Now(), lastUserInput: time.Now(),
log: logger, log: logger,
@ -163,10 +163,10 @@ func (u *UsbGadget) Close() error {
// Stop auto-release timer // Stop auto-release timer
u.kbdAutoReleaseLock.Lock() u.kbdAutoReleaseLock.Lock()
for _, timer := range u.kbdAutoReleaseTimers { if u.kbdAutoReleaseTimer != nil {
timer.Stop() u.kbdAutoReleaseTimer.Stop()
u.kbdAutoReleaseTimer = nil
} }
u.kbdAutoReleaseTimers = make(map[byte]*time.Timer)
u.kbdAutoReleaseLock.Unlock() u.kbdAutoReleaseLock.Unlock()
// Close HID files // Close HID files

View File

@ -1066,7 +1066,9 @@ var rpcHandlers = map[string]RPCHandler{
"getNetworkSettings": {Func: rpcGetNetworkSettings}, "getNetworkSettings": {Func: rpcGetNetworkSettings},
"setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}}, "setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}},
"renewDHCPLease": {Func: rpcRenewDHCPLease}, "renewDHCPLease": {Func: rpcRenewDHCPLease},
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
"getKeyboardLedState": {Func: rpcGetKeyboardLedState}, "getKeyboardLedState": {Func: rpcGetKeyboardLedState},
"keypressReport": {Func: rpcKeypressReport, Params: []string{"key", "press"}},
"getKeyDownState": {Func: rpcGetKeysDownState}, "getKeyDownState": {Func: rpcGetKeysDownState},
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}}, "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},

View File

@ -13,7 +13,10 @@ func initMdns() error {
networkState.GetHostname(), networkState.GetHostname(),
networkState.GetFQDN(), networkState.GetFQDN(),
}, },
ListenOptions: config.NetworkConfig.GetMDNSMode(), ListenOptions: &mdns.MDNSListenOptions{
IPv4: true,
IPv6: true,
},
}) })
if err != nil { if err != nil {
return err return err

View File

@ -17,7 +17,7 @@ export default function Ipv6NetworkCard({
</h3> </h3>
<div className="grid grid-cols-2 gap-x-6 gap-y-2"> <div className="grid grid-cols-2 gap-x-6 gap-y-2">
{networkState?.ipv6_link_local && ( {networkState?.dhcp_lease?.ip && (
<div className="flex flex-col justify-between"> <div className="flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
Link-local Link-local

View File

@ -237,7 +237,6 @@ export default function WebRTCVideo() {
const keyDownHandler = useCallback( const keyDownHandler = useCallback(
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
e.preventDefault(); e.preventDefault();
if (e.repeat) return;
const code = getAdjustedKeyCode(e); const code = getAdjustedKeyCode(e);
const hidKey = keys[code]; const hidKey = keys[code];

View File

@ -36,16 +36,11 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
}, [rpcHidChannel, rpcHidProtocolVersion]); }, [rpcHidChannel, rpcHidProtocolVersion]);
const rpcHidUnreliableReady = useMemo(() => { const rpcHidUnreliableReady = useMemo(() => {
return ( return rpcHidUnreliableChannel?.readyState === "open" && rpcHidProtocolVersion !== null;
rpcHidUnreliableChannel?.readyState === "open" && rpcHidProtocolVersion !== null
);
}, [rpcHidUnreliableChannel, rpcHidProtocolVersion]); }, [rpcHidUnreliableChannel, rpcHidProtocolVersion]);
const rpcHidUnreliableNonOrderedReady = useMemo(() => { const rpcHidUnreliableNonOrderedReady = useMemo(() => {
return ( return rpcHidUnreliableNonOrderedChannel?.readyState === "open" && rpcHidProtocolVersion !== null;
rpcHidUnreliableNonOrderedChannel?.readyState === "open" &&
rpcHidProtocolVersion !== null
);
}, [rpcHidUnreliableNonOrderedChannel, rpcHidProtocolVersion]); }, [rpcHidUnreliableNonOrderedChannel, rpcHidProtocolVersion]);
const rpcHidStatus = useMemo(() => { const rpcHidStatus = useMemo(() => {
@ -55,15 +50,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
return `ready (v${rpcHidProtocolVersion}${rpcHidUnreliableReady ? "+u" : ""})`; return `ready (v${rpcHidProtocolVersion}${rpcHidUnreliableReady ? "+u" : ""})`;
}, [rpcHidChannel, rpcHidUnreliableReady, rpcHidProtocolVersion]); }, [rpcHidChannel, rpcHidUnreliableReady, rpcHidProtocolVersion]);
const sendMessage = useCallback( const sendMessage = useCallback((message: RpcMessage, { ignoreHandshakeState, useUnreliableChannel, requireOrdered = true }: sendMessageParams = {}) => {
(
message: RpcMessage,
{
ignoreHandshakeState,
useUnreliableChannel,
requireOrdered = true,
}: sendMessageParams = {},
) => {
if (rpcHidChannel?.readyState !== "open") return; if (rpcHidChannel?.readyState !== "open") return;
if (!rpcHidReady && !ignoreHandshakeState) return; if (!rpcHidReady && !ignoreHandshakeState) return;
@ -85,22 +72,19 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
} }
rpcHidChannel?.send(data as unknown as ArrayBuffer); rpcHidChannel?.send(data as unknown as ArrayBuffer);
}, }, [
[
rpcHidChannel, rpcHidChannel,
rpcHidUnreliableChannel, rpcHidUnreliableChannel,
rpcHidUnreliableNonOrderedChannel, rpcHidUnreliableNonOrderedChannel,
rpcHidReady, rpcHidReady,
rpcHidUnreliableReady, rpcHidUnreliableReady,
rpcHidUnreliableNonOrderedReady, rpcHidUnreliableNonOrderedReady,
], ]);
);
const reportKeyboardEvent = useCallback( const reportKeyboardEvent = useCallback(
(keys: number[], modifier: number) => { (keys: number[], modifier: number) => {
sendMessage(new KeyboardReportMessage(keys, modifier)); sendMessage(new KeyboardReportMessage(keys, modifier));
}, }, [sendMessage],
[sendMessage],
); );
const reportKeypressEvent = useCallback( const reportKeypressEvent = useCallback(
@ -112,9 +96,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
const reportAbsMouseEvent = useCallback( const reportAbsMouseEvent = useCallback(
(x: number, y: number, buttons: number) => { (x: number, y: number, buttons: number) => {
sendMessage(new PointerReportMessage(x, y, buttons), { sendMessage(new PointerReportMessage(x, y, buttons), { useUnreliableChannel: true });
useUnreliableChannel: true,
});
}, },
[sendMessage], [sendMessage],
); );
@ -127,7 +109,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
); );
const reportKeypressKeepAlive = useCallback(() => { const reportKeypressKeepAlive = useCallback(() => {
sendMessage(KEEPALIVE_MESSAGE); sendMessage(KEEPALIVE_MESSAGE, { useUnreliableChannel: true, requireOrdered: false });
}, [sendMessage]); }, [sendMessage]);
const sendHandshake = useCallback(() => { const sendHandshake = useCallback(() => {
@ -137,8 +119,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
sendMessage(new HandshakeMessage(HID_RPC_VERSION), { ignoreHandshakeState: true }); sendMessage(new HandshakeMessage(HID_RPC_VERSION), { ignoreHandshakeState: true });
}, [rpcHidChannel, rpcHidProtocolVersion, sendMessage]); }, [rpcHidChannel, rpcHidProtocolVersion, sendMessage]);
const handleHandshake = useCallback( const handleHandshake = useCallback((message: HandshakeMessage) => {
(message: HandshakeMessage) => {
if (!message.version) { if (!message.version) {
console.error("Received handshake message without version", message); console.error("Received handshake message without version", message);
return; return;
@ -153,9 +134,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
} }
setRpcHidProtocolVersion(message.version); setRpcHidProtocolVersion(message.version);
}, }, [setRpcHidProtocolVersion]);
[setRpcHidProtocolVersion],
);
useEffect(() => { useEffect(() => {
if (!rpcHidChannel) return; if (!rpcHidChannel) return;
@ -189,12 +168,12 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
}; };
const openHandler = () => { const openHandler = () => {
console.info("HID RPC channel opened"); console.warn("HID RPC channel opened");
sendHandshake(); sendHandshake();
}; };
const closeHandler = () => { const closeHandler = () => {
console.info("HID RPC channel closed"); console.warn("HID RPC channel closed");
setRpcHidProtocolVersion(null); setRpcHidProtocolVersion(null);
}; };
@ -207,13 +186,15 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
rpcHidChannel.removeEventListener("close", closeHandler); rpcHidChannel.removeEventListener("close", closeHandler);
rpcHidChannel.removeEventListener("open", openHandler); rpcHidChannel.removeEventListener("open", openHandler);
}; };
}, [ },
[
rpcHidChannel, rpcHidChannel,
onHidRpcMessage, onHidRpcMessage,
setRpcHidProtocolVersion, setRpcHidProtocolVersion,
sendHandshake, sendHandshake,
handleHandshake, handleHandshake,
]); ],
);
return { return {
reportKeyboardEvent, reportKeyboardEvent,

View File

@ -1,12 +1,6 @@
import { useCallback, useRef } from "react"; import { useCallback, useRef } from "react";
import { import { hidErrorRollOver, hidKeyBufferSize, KeysDownState, useHidStore, useRTCStore } from "@/hooks/stores";
hidErrorRollOver,
hidKeyBufferSize,
KeysDownState,
useHidStore,
useRTCStore,
} from "@/hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useHidRpc } from "@/hooks/useHidRpc"; import { useHidRpc } from "@/hooks/useHidRpc";
import { KeyboardLedStateMessage, KeysDownStateMessage } from "@/hooks/hidRpc"; import { KeyboardLedStateMessage, KeysDownStateMessage } from "@/hooks/hidRpc";
@ -37,7 +31,7 @@ export default function useKeyboard() {
reportKeypressEvent: sendKeypressEventHidRpc, reportKeypressEvent: sendKeypressEventHidRpc,
reportKeypressKeepAlive: sendKeypressKeepAliveHidRpc, reportKeypressKeepAlive: sendKeypressKeepAliveHidRpc,
rpcHidReady, rpcHidReady,
} = useHidRpc(message => { } = useHidRpc((message) => {
switch (message.constructor) { switch (message.constructor) {
case KeysDownStateMessage: case KeysDownStateMessage:
setKeysDownState((message as KeysDownStateMessage).keysDownState); setKeysDownState((message as KeysDownStateMessage).keysDownState);
@ -58,9 +52,7 @@ export default function useKeyboard() {
async (state: KeysDownState) => { async (state: KeysDownState) => {
if (rpcDataChannel?.readyState !== "open" && !rpcHidReady) return; if (rpcDataChannel?.readyState !== "open" && !rpcHidReady) return;
console.debug( console.debug(`Send keyboardReport keys: ${state.keys}, modifier: ${state.modifier}`);
`Send keyboardReport keys: ${state.keys}, modifier: ${state.modifier}`,
);
if (rpcHidReady) { if (rpcHidReady) {
console.debug("Sending keyboard report via HidRPC"); console.debug("Sending keyboard report via HidRPC");
@ -68,18 +60,20 @@ export default function useKeyboard() {
return; return;
} }
send( send("keyboardReport", { keys: state.keys, modifier: state.modifier }, (resp: JsonRpcResponse) => {
"keyboardReport",
{ keys: state.keys, modifier: state.modifier },
(resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
console.error(`Failed to send keyboard report ${state}`, resp.error); 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. // executeMacro is used to execute a macro consisting of multiple steps.
// Each step can have multiple keys, multiple modifiers and a delay. // Each step can have multiple keys, multiple modifiers and a delay.
@ -87,14 +81,10 @@ export default function useKeyboard() {
// After the delay, the keys and modifiers are released and the next step is executed. // 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. // 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. // A small pause is added between steps to ensure that the device can process the events.
const executeMacro = async ( const executeMacro = async (steps: { keys: string[] | null; modifiers: string[] | null; delay: number }[]) => {
steps: { keys: string[] | null; modifiers: string[] | null; delay: number }[],
) => {
for (const [index, step] of steps.entries()) { for (const [index, 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]).reduce((acc, val) => acc + val, 0);
.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 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) {
@ -129,8 +119,6 @@ export default function useKeyboard() {
clearInterval(keepAliveTimerRef.current); clearInterval(keepAliveTimerRef.current);
} }
sendKeypressKeepAliveHidRpc();
// Create new interval timer // Create new interval timer
keepAliveTimerRef.current = setInterval(() => { keepAliveTimerRef.current = setInterval(() => {
sendKeypressKeepAliveHidRpc(); sendKeypressKeepAliveHidRpc();
@ -186,11 +174,7 @@ export default function useKeyboard() {
sendKeypress(key, press); sendKeypress(key, press);
} else { } else {
// if the keyPress api is not available, we need to handle the key locally // if the keyPress api is not available, we need to handle the key locally
const downState = simulateDeviceSideKeyHandlingForLegacyDevices( const downState = simulateDeviceSideKeyHandlingForLegacyDevices(keysDownState, key, press);
keysDownState,
key,
press,
);
sendKeyboardEvent(downState); // then we send the full state sendKeyboardEvent(downState); // then we send the full state
// if we just sent ErrorRollOver, reset to empty state // if we just sent ErrorRollOver, reset to empty state
@ -210,11 +194,7 @@ export default function useKeyboard() {
); );
// IMPORTANT: See the keyPressReportApiAvailable comment above for the reason this exists // IMPORTANT: See the keyPressReportApiAvailable comment above for the reason this exists
function simulateDeviceSideKeyHandlingForLegacyDevices( function simulateDeviceSideKeyHandlingForLegacyDevices(state: KeysDownState, key: number, press: boolean): KeysDownState {
state: KeysDownState,
key: number,
press: boolean,
): KeysDownState {
// IMPORTANT: This code parallels the logic in the kernel's hid-gadget driver // 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 // for handling key presses and releases. It ensures that the USB gadget
// behaves similarly to a real USB HID keyboard. This logic is paralleled // behaves similarly to a real USB HID keyboard. This logic is paralleled
@ -243,7 +223,7 @@ export default function useKeyboard() {
// 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)
if (keys[i] === key || keys[i] === 0) { if (keys[i] === key || keys[i] === 0) {
if (press) { 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 { } else {
// 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) {
@ -259,15 +239,13 @@ export default function useKeyboard() {
// If we reach here it means we didn't find an empty slot or the key in the buffer // If we reach here it means we didn't find an empty slot or the key in the buffer
if (overrun) { if (overrun) {
if (press) { if (press) {
console.warn( console.warn(`keyboard buffer overflow current keys ${keys}, key: ${key} not added`);
`keyboard buffer overflow current keys ${keys}, key: ${key} not added`,
);
// Fill all key slots with ErrorRollOver (0x01) to indicate overflow // Fill all key slots with ErrorRollOver (0x01) to indicate overflow
keys.length = hidKeyBufferSize; keys.length = hidKeyBufferSize;
keys.fill(hidErrorRollOver); keys.fill(hidErrorRollOver);
} else { } else {
// If we are releasing a key, and we didn't find it in a slot, who cares? // 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`)
} }
} }
} }

View File

@ -166,11 +166,11 @@ export default function SettingsNetworkRoute() {
}, [getNetworkState, getNetworkSettings]); }, [getNetworkState, getNetworkSettings]);
const handleIpv4ModeChange = (value: IPv4Mode | string) => { const handleIpv4ModeChange = (value: IPv4Mode | string) => {
setNetworkSettingsRemote({ ...networkSettings, ipv4_mode: value as IPv4Mode }); setNetworkSettings({ ...networkSettings, ipv4_mode: value as IPv4Mode });
}; };
const handleIpv6ModeChange = (value: IPv6Mode | string) => { const handleIpv6ModeChange = (value: IPv6Mode | string) => {
setNetworkSettingsRemote({ ...networkSettings, ipv6_mode: value as IPv6Mode }); setNetworkSettings({ ...networkSettings, ipv6_mode: value as IPv6Mode });
}; };
const handleLldpModeChange = (value: LLDPMode | string) => { const handleLldpModeChange = (value: LLDPMode | string) => {
@ -419,7 +419,7 @@ export default function SettingsNetworkRoute() {
value={networkSettings.ipv6_mode} value={networkSettings.ipv6_mode}
onChange={e => handleIpv6ModeChange(e.target.value)} onChange={e => handleIpv6ModeChange(e.target.value)}
options={filterUnknown([ options={filterUnknown([
{ value: "disabled", label: "Disabled" }, // { value: "disabled", label: "Disabled" },
{ value: "slaac", label: "SLAAC" }, { value: "slaac", label: "SLAAC" },
// { value: "dhcpv6", label: "DHCPv6" }, // { value: "dhcpv6", label: "DHCPv6" },
// { value: "slaac_and_dhcpv6", label: "SLAAC and DHCPv6" }, // { value: "slaac_and_dhcpv6", label: "SLAAC and DHCPv6" },

6
usb.go
View File

@ -33,7 +33,7 @@ func initUsbGadget() {
gadget.SetOnKeysDownChange(func(state usbgadget.KeysDownState) { gadget.SetOnKeysDownChange(func(state usbgadget.KeysDownState) {
if currentSession != nil { if currentSession != nil {
currentSession.enqueueKeysDownState(state) currentSession.reportHidRPCKeysDownState(state)
} }
}) })
@ -43,11 +43,11 @@ func initUsbGadget() {
} }
} }
func rpcKeyboardReport(modifier byte, keys []byte) error { func rpcKeyboardReport(modifier byte, keys []byte) (usbgadget.KeysDownState, error) {
return gadget.KeyboardReport(modifier, keys) return gadget.KeyboardReport(modifier, keys)
} }
func rpcKeypressReport(key byte, press bool) error { func rpcKeypressReport(key byte, press bool) (usbgadget.KeysDownState, error) {
return gadget.KeypressReport(key, press) return gadget.KeypressReport(key, press)
} }

25
web.go
View File

@ -562,31 +562,14 @@ func RunWebServer() {
r := setupRouter() r := setupRouter()
// Determine the binding address based on the config // Determine the binding address based on the config
var bindAddress string bindAddress := ":80" // Default to all interfaces
listenPort := 80 // default port
useIPv4 := config.NetworkConfig.IPv4Mode.String != "disabled"
useIPv6 := config.NetworkConfig.IPv6Mode.String != "disabled"
if config.LocalLoopbackOnly { if config.LocalLoopbackOnly {
if useIPv4 && useIPv6 { bindAddress = "localhost:80" // Loopback only (both IPv4 and IPv6)
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") logger.Info().Str("bindAddress", bindAddress).Bool("loopbackOnly", config.LocalLoopbackOnly).Msg("Starting web server")
if err := r.Run(bindAddress); err != nil { err := r.Run(bindAddress)
if err != nil {
panic(err) panic(err)
} }
} }

View File

@ -13,7 +13,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/jetkvm/kvm/internal/hidrpc" "github.com/jetkvm/kvm/internal/hidrpc"
"github.com/jetkvm/kvm/internal/logging" "github.com/jetkvm/kvm/internal/logging"
"github.com/jetkvm/kvm/internal/usbgadget"
"github.com/pion/webrtc/v4" "github.com/pion/webrtc/v4"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
@ -31,8 +30,6 @@ type Session struct {
hidRPCAvailable bool hidRPCAvailable bool
hidQueueLock sync.Mutex hidQueueLock sync.Mutex
hidQueue []chan hidQueueMessage hidQueue []chan hidQueueMessage
keysDownStateQueue chan usbgadget.KeysDownState
} }
type hidQueueMessage struct { type hidQueueMessage struct {
@ -99,32 +96,6 @@ 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) { func getOnHidMessageHandler(session *Session, scopedLogger *zerolog.Logger, channel string) func(msg webrtc.DataChannelMessage) {
return func(msg webrtc.DataChannelMessage) { return func(msg webrtc.DataChannelMessage) {
l := scopedLogger.With(). l := scopedLogger.With().
@ -210,7 +181,6 @@ 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.initQueues()
session.initKeysDownStateQueue()
go func() { go func() {
for msg := range session.rpcQueue { for msg := range session.rpcQueue {