diff --git a/internal/usbgadget/consts.go b/internal/usbgadget/consts.go index 958aecca..afded8fa 100644 --- a/internal/usbgadget/consts.go +++ b/internal/usbgadget/consts.go @@ -4,4 +4,13 @@ import "time" const dwc3Path = "/sys/bus/platform/drivers/dwc3" -const hidWriteTimeout = 10 * time.Millisecond +// Timeout for HID writes to gadget endpoints. Some hosts poll at ~10ms, but +// under load this can be longer. Use a slightly higher value to reduce +// spurious timeouts while still preventing indefinite blocking. +const hidWriteTimeout = 50 * time.Millisecond + +// Auto-release configuration. When keys are down and no successful HID write +// has happened for this duration, clear the keyboard state to prevent stuck +// keys after network hiccups. +const keyAutoReleaseAfter = 2 * time.Second +const keyAutoReleaseCheckInterval = 200 * time.Millisecond diff --git a/internal/usbgadget/hid_keyboard.go b/internal/usbgadget/hid_keyboard.go index fb710c20..9d30e9a1 100644 --- a/internal/usbgadget/hid_keyboard.go +++ b/internal/usbgadget/hid_keyboard.go @@ -163,9 +163,16 @@ func (u *UsbGadget) updateKeyDownState(state KeysDownState) { } 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") + // Call the callback asynchronously to avoid blocking the HID RPC handler path + // and to prevent timeouts when the network is slow or the channel back-pressures. + st := state + // make a copy of the slice to avoid data races + st.Keys = append([]byte(nil), state.Keys...) + go func(s KeysDownState) { + u.log.Trace().Interface("state", s).Msg("calling onKeysDownChange") + (*u.onKeysDownChange)(s) + u.log.Trace().Interface("state", s).Msg("onKeysDownChange called") + }(st) } } @@ -234,6 +241,8 @@ func (u *UsbGadget) openKeyboardHidFile() error { u.keyboardStateCtx, u.keyboardStateCancel = context.WithCancel(context.Background()) u.listenKeyboardEvents() + // Start auto-release monitor to clear stuck keys after inactivity + u.listenKeyboardAutoRelease() return nil } @@ -400,3 +409,59 @@ func (u *UsbGadget) KeypressReport(key byte, press bool) (KeysDownState, error) return u.UpdateKeysDown(modifier, keys), err } + +// listenKeyboardAutoRelease starts a background monitor that automatically +// releases all keys if no input has been successfully written to the HID device +// for a period of time. This acts as a safety net to avoid stuck keys when +// key-up events are lost due to network issues. +func (u *UsbGadget) listenKeyboardAutoRelease() { + var path string + if u.keyboardHidFile != nil { + path = u.keyboardHidFile.Name() + } + l := u.log.With().Str("listener", "keyboardAutoRelease").Str("path", path).Logger() + l.Trace().Msg("starting") + + go func() { + ticker := time.NewTicker(keyAutoReleaseCheckInterval) + defer ticker.Stop() + for { + select { + case <-u.keyboardStateCtx.Done(): + l.Info().Msg("context done") + return + case <-ticker.C: + // Fast path: if nothing is currently down, skip work + u.keyboardStateLock.Lock() + modifier := u.keysDownState.Modifier + keys := append([]byte(nil), u.keysDownState.Keys...) + u.keyboardStateLock.Unlock() + + allZero := modifier == 0 + if allZero { + for _, k := range keys { + if k != 0 { + allZero = false + break + } + } + } + if allZero { + continue + } + + // If we have any key/modifier down and we've been idle long enough, clear state + if time.Since(u.GetLastUserInputTime()) >= keyAutoReleaseAfter { + l.Debug().Dur("idle", time.Since(u.GetLastUserInputTime())).Msg("auto-releasing stuck keys") + zeros := make([]byte, hidKeyBufferSize) + _, err := u.KeyboardReport(0x00, zeros) + if err != nil { + u.logWithSuppression("keyboardAutoReleaseWrite", 100, &l, err, "failed to auto-release keys") + } else { + u.resetLogSuppressionCounter("keyboardAutoReleaseWrite") + } + } + } + } + }() +} diff --git a/internal/usbgadget/utils.go b/internal/usbgadget/utils.go index d51f9e40..027f2d2c 100644 --- a/internal/usbgadget/utils.go +++ b/internal/usbgadget/utils.go @@ -116,23 +116,22 @@ func (u *UsbGadget) writeWithTimeout(file *os.File, data []byte) (n int, err err } n, err = file.Write(data) - if err == nil { - return + if err != nil { + if errors.Is(err, os.ErrDeadlineExceeded) { + // Promote visibility of timeouts and allow callers to recover by + // reopening the file. Do not swallow the error. + u.logWithSuppression( + fmt.Sprintf("writeWithTimeout_%s", file.Name()), + 100, + u.log, + err, + "write timed out: %s", + file.Name(), + ) + } + return n, err } - - if errors.Is(err, os.ErrDeadlineExceeded) { - u.logWithSuppression( - fmt.Sprintf("writeWithTimeout_%s", file.Name()), - 1000, - u.log, - err, - "write timed out: %s", - file.Name(), - ) - err = nil - } - - return + return n, nil } func (u *UsbGadget) logWithSuppression(counterName string, every int, logger *zerolog.Logger, err error, msg string, args ...any) { diff --git a/webrtc.go b/webrtc.go index c3d0dc1b..c5dc6736 100644 --- a/webrtc.go +++ b/webrtc.go @@ -274,6 +274,17 @@ func newSession(config SessionConfig) (*Session, error) { session.rpcQueue = nil } + // Best-effort: clear any potentially stuck keys on channel close + if gadget != nil { + go func() { + zeros := make([]byte, 6) + _, err := rpcKeyboardReport(0x00, zeros) + if err != nil { + scopedLogger.Debug().Err(err).Msg("failed to auto-release keys on channel close") + } + }() + } + // Stop HID RPC processor for i := 0; i < len(session.hidQueue); i++ { close(session.hidQueue[i])