mirror of https://github.com/jetkvm/kvm.git
feat: Add HID keyboard auto-release and async callbacks
Co-authored-by: adam <adam@buildjet.com>
This commit is contained in:
parent
1717549578
commit
20783e6118
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
11
webrtc.go
11
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])
|
||||
|
|
Loading…
Reference in New Issue